Plugin-Lösungen für MCP-Server implementieren
Aufbauend auf der MCP-Server-Architektur, die wir zuvor untersucht haben, besteht der nächste Schritt darin, Ihren Server durch die Bereitstellung von Tools, Resources und Prompts über ein Plugin-System wirklich erweiterbar zu machen. Die Stärke von MCP liegt in seiner Fähigkeit, Server Capabilities bereitzustellen, die KI-Modelle entdecken und nutzen können, und Plugins bieten den perfekten Mechanismus zur Organisation dieser Capabilities.
Warum MCP-Plugin-Architektur?
MCP-Server stellen drei Kern-Capabilities für Clients bereit: Tools (Aktionen, die das Modell aufrufen kann), Resources (Daten, auf die das Modell zugreifen kann) und Prompts (wiederverwendbare Anweisungsvorlagen). Wenn Ihr Server wächst, wird die Verwaltung all dieser Capabilities an einem Ort unhandlich. Eine Plugin-Architektur löst dies durch:
- Modulare Capabilities: Jedes Plugin stellt seine eigenen Tools, Resources und Prompts bereit
- Unabhängige Entwicklung: Teams können Capability-Plugins entwickeln, ohne sich bei Kernänderungen abstimmen zu müssen
- Dynamische Registrierung: MCP-Capabilities zur Laufzeit hinzufügen oder entfernen ohne Neubereitstellung
- Erweiterungen von Drittanbietern: Externe Entwickler können Ihren MCP-Server mit neuen Capabilities erweitern
- Test-Isolation: Capability-Plugins unabhängig vom Kernsystem testen
Betrachten Sie ein MCP-Plugin als eigenständiges Modul, das Tools, Resources und Prompts bei Ihrem Server registriert:
interface MCPPlugin {
name: string
version: string
initialize(context: PluginContext): Promise<void>
start(): Promise<void>
stop(): Promise<void>
destroy(): Promise<void>
}
interface PluginContext {
server: MCPServer
config: PluginConfig
logger: Logger
registerTool(tool: ToolDefinition): void
registerResource(resource: ResourceDefinition): void
registerPrompt(prompt: PromptDefinition): void
}
interface ToolDefinition {
name: string
description: string
inputSchema: JSONSchema
handler: (params: unknown) => Promise<unknown>
}
interface ResourceDefinition {
uri: string
name: string
description: string
mimeType: string
handler: () => Promise<string | Buffer>
}
interface PromptDefinition {
name: string
description: string
arguments: PromptArgument[]
handler: (args: Record<string, unknown>) => Promise<PromptMessage[]>
}
MCP-Capabilities verstehen
Bevor wir uns mit Plugins befassen, klären wir, was jede MCP-Capability bietet:
Tools sind Aktionen, die Ihr Server bereitstellt und die das KI-Modell aufrufen kann. Jedes Tool hat einen schema-definierten Input und führt eine bestimmte Operation aus. Tools erfordern eine explizite Benutzerfreigabe vor der Ausführung, um sicherzustellen, dass Benutzer die Kontrolle behalten.
Resources sind schreibgeschützte Daten, die Ihr Server dem Modell zur Verfügung stellt. Sie können Dateien, Datenbankeinträge, API-Antworten oder beliebige strukturierte Informationen sein. Resources werden durch URIs identifiziert und deklarieren MIME-Typen.
Prompts sind wiederverwendbare Anweisungsvorlagen, die KI-Interaktionen leiten. Sie standardisieren, wie Modelle häufige Aufgaben ausführen, und können dynamische Argumente enthalten, die Clients zur Laufzeit ausfüllen.
MCP-Capability-Registry
Eine Capability-Registry verwaltet alle Tools, Resources und Prompts, die von Plugins bereitgestellt werden:
class MCPCapabilityRegistry {
private tools: Map<string, ToolDefinition> = new Map()
private resources: Map<string, ResourceDefinition> = new Map()
private prompts: Map<string, PromptDefinition> = new Map()
registerTool(tool: ToolDefinition): void {
if (this.tools.has(tool.name)) {
throw new Error(`Tool ${tool.name} already registered`)
}
this.tools.set(tool.name, tool)
}
registerResource(resource: ResourceDefinition): void {
if (this.resources.has(resource.uri)) {
throw new Error(`Resource ${resource.uri} already registered`)
}
this.resources.set(resource.uri, resource)
}
registerPrompt(prompt: PromptDefinition): void {
if (this.prompts.has(prompt.name)) {
throw new Error(`Prompt ${prompt.name} already registered`)
}
this.prompts.set(prompt.name, prompt)
}
listTools(): ToolDefinition[] {
return Array.from(this.tools.values())
}
listResources(): ResourceDefinition[] {
return Array.from(this.resources.values())
}
listPrompts(): PromptDefinition[] {
return Array.from(this.prompts.values())
}
getTool(name: string): ToolDefinition | undefined {
return this.tools.get(name)
}
getResource(uri: string): ResourceDefinition | undefined {
return this.resources.get(uri)
}
getPrompt(name: string): PromptDefinition | undefined {
return this.prompts.get(name)
}
unregisterTool(name: string): void {
this.tools.delete(name)
}
unregisterResource(uri: string): void {
this.resources.delete(uri)
}
unregisterPrompt(name: string): void {
this.prompts.delete(name)
}
}
Dynamisches Plugin-Laden
Laden Sie MCP-Plugins dynamisch, um Capabilities zur Laufzeit hinzuzufügen:
class MCPPluginLoader {
constructor(
private registry: MCPCapabilityRegistry,
private pluginDir: string
) {}
async loadPlugin(pluginPath: string): Promise<MCPPlugin> {
try {
const absolutePath = path.resolve(this.pluginDir, pluginPath)
if (!this.isValidPluginPath(absolutePath)) {
throw new Error('Invalid plugin path')
}
const pluginModule = await import(absolutePath)
if (!this.isValidMCPPlugin(pluginModule.default)) {
throw new Error('Invalid MCP plugin structure')
}
const plugin = pluginModule.default as MCPPlugin
return plugin
} catch (error) {
throw new Error(`Failed to load MCP plugin: ${error.message}`)
}
}
async loadAllPlugins(): Promise<MCPPlugin[]> {
const pluginFiles = await this.discoverPlugins()
const plugins: MCPPlugin[] = []
for (const file of pluginFiles) {
try {
const plugin = await this.loadPlugin(file)
plugins.push(plugin)
} catch (error) {
console.error(`Failed to load plugin ${file}:`, error)
}
}
return plugins
}
private async discoverPlugins(): Promise<string[]> {
const files = await fs.readdir(this.pluginDir)
return files.filter(file =>
file.endsWith('.js') || file.endsWith('.ts')
)
}
private isValidPluginPath(pluginPath: string): boolean {
const normalized = path.normalize(pluginPath)
return normalized.startsWith(path.normalize(this.pluginDir))
}
private isValidMCPPlugin(plugin: any): boolean {
return (
plugin &&
typeof plugin.name === 'string' &&
typeof plugin.version === 'string' &&
typeof plugin.initialize === 'function' &&
typeof plugin.start === 'function' &&
typeof plugin.stop === 'function' &&
typeof plugin.destroy === 'function'
)
}
}
Plugin-Lebenszyklus-Verwaltung
Verwalten Sie den Lebenszyklus von MCP-Plugins, um eine ordnungsgemäße Initialisierung und Bereinigung sicherzustellen:
class MCPPluginLifecycleManager {
private plugins: Map<string, MCPPlugin> = new Map()
async initializePlugin(
plugin: MCPPlugin,
context: PluginContext
): Promise<void> {
try {
await plugin.initialize(context)
this.plugins.set(plugin.name, plugin)
} catch (error) {
throw new Error(
`Failed to initialize plugin ${plugin.name}: ${error.message}`
)
}
}
async startPlugin(pluginName: string): Promise<void> {
const plugin = this.plugins.get(pluginName)
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`)
}
try {
await plugin.start()
} catch (error) {
throw new Error(`Failed to start plugin ${pluginName}: ${error.message}`)
}
}
async stopPlugin(pluginName: string): Promise<void> {
const plugin = this.plugins.get(pluginName)
if (!plugin) return
try {
await plugin.stop()
} catch (error) {
console.error(`Error stopping plugin ${pluginName}:`, error)
}
}
async destroyPlugin(pluginName: string): Promise<void> {
const plugin = this.plugins.get(pluginName)
if (!plugin) return
try {
await plugin.destroy()
this.plugins.delete(pluginName)
} catch (error) {
console.error(`Error destroying plugin ${pluginName}:`, error)
}
}
async startAll(): Promise<void> {
for (const plugin of this.plugins.values()) {
await this.startPlugin(plugin.name)
}
}
async stopAll(): Promise<void> {
const plugins = Array.from(this.plugins.values()).reverse()
for (const plugin of plugins) {
await this.stopPlugin(plugin.name)
}
}
async destroyAll(): Promise<void> {
const plugins = Array.from(this.plugins.values()).reverse()
for (const plugin of plugins) {
await this.destroyPlugin(plugin.name)
}
}
}
Praxisnahe MCP-Plugin-Beispiele
Schauen wir uns praktische MCP-Plugins an, die Tools, Resources und Prompts bereitstellen:
Datenbank-Plugin: Stellt Datenbankoperationen als MCP-Tools und Daten als Resources bereit.
class DatabasePlugin implements MCPPlugin {
name = 'database'
version = '1.0.0'
private db: Database | null = null
async initialize(context: PluginContext): Promise<void> {
this.db = new Database(context.config.settings.connectionString)
context.registerTool({
name: 'query_database',
description: 'Execute a SQL query against the database',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'SQL query to execute' },
params: { type: 'array', description: 'Query parameters' }
},
required: ['query']
},
handler: async (params: any) => {
const results = await this.db!.query(params.query, params.params || [])
return { rows: results, count: results.length }
}
})
context.registerResource({
uri: 'db://schema',
name: 'database_schema',
description: 'Database schema information',
mimeType: 'application/json',
handler: async () => {
const schema = await this.db!.getSchema()
return JSON.stringify(schema, null, 2)
}
})
context.registerPrompt({
name: 'generate_query',
description: 'Generate a SQL query based on natural language description',
arguments: [
{
name: 'description',
description: 'Natural language description of the query',
required: true
}
],
handler: async (args) => {
return [
{
role: 'user',
content: {
type: 'text',
text: `Generate a SQL query for: ${args.description}\n\nUse the database schema from the db://schema resource.`
}
}
]
}
})
}
async start(): Promise<void> {
await this.db!.connect()
}
async stop(): Promise<void> {
await this.db!.disconnect()
}
async destroy(): Promise<void> {
this.db = null
}
}
Dateisystem-Plugin: Stellt Dateioperationen als Tools und Dateien als Resources bereit.
class FileSystemPlugin implements MCPPlugin {
name = 'filesystem'
version = '1.0.0'
private rootPath: string = ''
async initialize(context: PluginContext): Promise<void> {
this.rootPath = context.config.settings.rootPath
context.registerTool({
name: 'read_file',
description: 'Read contents of a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path relative to root' }
},
required: ['path']
},
handler: async (params: any) => {
const fullPath = path.join(this.rootPath, params.path)
const content = await fs.readFile(fullPath, 'utf-8')
return { content, path: params.path }
}
})
context.registerTool({
name: 'write_file',
description: 'Write contents to a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path relative to root' },
content: { type: 'string', description: 'File content to write' }
},
required: ['path', 'content']
},
handler: async (params: any) => {
const fullPath = path.join(this.rootPath, params.path)
await fs.writeFile(fullPath, params.content, 'utf-8')
return { success: true, path: params.path }
}
})
context.registerResource({
uri: 'file://directory-listing',
name: 'directory_listing',
description: 'List of all files in the root directory',
mimeType: 'application/json',
handler: async () => {
const files = await this.listFiles(this.rootPath)
return JSON.stringify(files, null, 2)
}
})
}
private async listFiles(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
const subFiles = await this.listFiles(fullPath)
files.push(...subFiles)
} else {
files.push(path.relative(this.rootPath, fullPath))
}
}
return files
}
async start(): Promise<void> {
// Verify root path exists
await fs.access(this.rootPath)
}
async stop(): Promise<void> {
// Cleanup if needed
}
async destroy(): Promise<void> {
this.rootPath = ''
}
}
API-Integrations-Plugin: Stellt externe API-Aufrufe als Tools und API-Daten als Resources bereit.
class WeatherAPIPlugin implements MCPPlugin {
name = 'weather'
version = '1.0.0'
private apiKey: string = ''
private apiClient: any = null
async initialize(context: PluginContext): Promise<void> {
this.apiKey = context.config.settings.apiKey
this.apiClient = new WeatherAPIClient(this.apiKey)
context.registerTool({
name: 'get_weather',
description: 'Get current weather for a location',
inputSchema: {
type: 'object',
properties: {
location: { type: 'string', description: 'City name or coordinates' },
units: {
type: 'string',
enum: ['metric', 'imperial'],
description: 'Temperature units'
}
},
required: ['location']
},
handler: async (params: any) => {
const weather = await this.apiClient.getCurrentWeather(
params.location,
params.units || 'metric'
)
return weather
}
})
context.registerTool({
name: 'get_forecast',
description: 'Get weather forecast for a location',
inputSchema: {
type: 'object',
properties: {
location: { type: 'string', description: 'City name or coordinates' },
days: { type: 'number', description: 'Number of days (1-7)' }
},
required: ['location']
},
handler: async (params: any) => {
const forecast = await this.apiClient.getForecast(
params.location,
params.days || 3
)
return forecast
}
})
context.registerPrompt({
name: 'weather_report',
description: 'Generate a weather report for a location',
arguments: [
{
name: 'location',
description: 'Location for the weather report',
required: true
}
],
handler: async (args) => {
return [
{
role: 'user',
content: {
type: 'text',
text: `Use the get_weather and get_forecast tools to create a comprehensive weather report for ${args.location}. Include current conditions and a 3-day forecast.`
}
}
]
}
})
}
async start(): Promise<void> {
// Verify API key is valid
await this.apiClient.testConnection()
}
async stop(): Promise<void> {
// Cleanup connections
}
async destroy(): Promise<void> {
this.apiClient = null
}
}
MCP-Protokollmethoden verarbeiten
Ihr MCP-Server muss Protokollmethoden verarbeiten, die Capabilities auflisten und aufrufen:
class MCPServer {
constructor(
private registry: MCPCapabilityRegistry,
private lifecycle: MCPPluginLifecycleManager
) {}
async handleRequest(request: MCPRequest): Promise<MCPResponse> {
switch (request.method) {
case 'tools/list':
return this.handleToolsList()
case 'tools/call':
return this.handleToolsCall(request.params)
case 'resources/list':
return this.handleResourcesList()
case 'resources/read':
return this.handleResourcesRead(request.params)
case 'prompts/list':
return this.handlePromptsList()
case 'prompts/get':
return this.handlePromptsGet(request.params)
default:
return {
id: request.id,
error: {
code: -32601,
message: `Method not found: ${request.method}`
}
}
}
}
private async handleToolsList(): Promise<MCPResponse> {
const tools = this.registry.listTools().map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}))
return {
id: 'tools-list',
result: { tools }
}
}
private async handleToolsCall(params: any): Promise<MCPResponse> {
const tool = this.registry.getTool(params.name)
if (!tool) {
return {
id: 'tools-call',
error: {
code: -32602,
message: `Tool not found: ${params.name}`
}
}
}
try {
const result = await tool.handler(params.arguments)
return {
id: 'tools-call',
result: {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
}
}
} catch (error) {
return {
id: 'tools-call',
error: {
code: -32000,
message: error.message
}
}
}
}
private async handleResourcesList(): Promise<MCPResponse> {
const resources = this.registry.listResources().map(resource => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
}))
return {
id: 'resources-list',
result: { resources }
}
}
private async handleResourcesRead(params: any): Promise<MCPResponse> {
const resource = this.registry.getResource(params.uri)
if (!resource) {
return {
id: 'resources-read',
error: {
code: -32602,
message: `Resource not found: ${params.uri}`
}
}
}
try {
const content = await resource.handler()
return {
id: 'resources-read',
result: {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
text: typeof content === 'string' ? content : content.toString()
}
]
}
}
} catch (error) {
return {
id: 'resources-read',
error: {
code: -32000,
message: error.message
}
}
}
}
private async handlePromptsList(): Promise<MCPResponse> {
const prompts = this.registry.listPrompts().map(prompt => ({
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments
}))
return {
id: 'prompts-list',
result: { prompts }
}
}
private async handlePromptsGet(params: any): Promise<MCPResponse> {
const prompt = this.registry.getPrompt(params.name)
if (!prompt) {
return {
id: 'prompts-get',
error: {
code: -32602,
message: `Prompt not found: ${params.name}`
}
}
}
try {
const messages = await prompt.handler(params.arguments || {})
return {
id: 'prompts-get',
result: {
description: prompt.description,
messages
}
}
} catch (error) {
return {
id: 'prompts-get',
error: {
code: -32000,
message: error.message
}
}
}
}
}
MCP-Plugins testen
Testen Sie Plugins, indem Sie überprüfen, ob sie MCP-Capabilities korrekt registrieren und bereitstellen:
describe('MCP Plugin System', () => {
let registry: MCPCapabilityRegistry
let loader: MCPPluginLoader
let lifecycle: MCPPluginLifecycleManager
beforeEach(() => {
registry = new MCPCapabilityRegistry()
loader = new MCPPluginLoader(registry, './plugins')
lifecycle = new MCPPluginLifecycleManager()
})
it('should register tools from plugin', async () => {
const plugin = new DatabasePlugin()
const context = createMockContext(registry)
await lifecycle.initializePlugin(plugin, context)
const tools = registry.listTools()
expect(tools).toHaveLength(1)
expect(tools[0].name).toBe('query_database')
})
it('should register resources from plugin', async () => {
const plugin = new DatabasePlugin()
const context = createMockContext(registry)
await lifecycle.initializePlugin(plugin, context)
const resources = registry.listResources()
expect(resources).toHaveLength(1)
expect(resources[0].uri).toBe('db://schema')
})
it('should register prompts from plugin', async () => {
const plugin = new DatabasePlugin()
const context = createMockContext(registry)
await lifecycle.initializePlugin(plugin, context)
const prompts = registry.listPrompts()
expect(prompts).toHaveLength(1)
expect(prompts[0].name).toBe('generate_query')
})
it('should invoke tool handler', async () => {
const plugin = new DatabasePlugin()
const context = createMockContext(registry)
await lifecycle.initializePlugin(plugin, context)
await lifecycle.startPlugin(plugin.name)
const tool = registry.getTool('query_database')
const result = await tool!.handler({ query: 'SELECT * FROM users' })
expect(result).toHaveProperty('rows')
expect(result).toHaveProperty('count')
})
})
Best Practices für MCP-Plugins
Best Practices:
- Entwerfen Sie Tools mit klaren, zweckorientierten Operationen
- Verwenden Sie beschreibende Namen und detaillierte Beschreibungen für die Auffindbarkeit
- Stellen Sie umfassende JSON-Schemas für Tool-Inputs bereit
- Machen Sie Resources leichtgewichtig und cachebar, wenn möglich
- Erstellen Sie Prompts, die das Modell zu effektiver Tool-Nutzung führen
- Behandeln Sie Fehler elegant und geben Sie aussagekräftige Fehlermeldungen zurück
- Dokumentieren Sie erwartete Tool-Ausführungszeiten für den Kontext der Benutzerfreigabe
- Versionieren Sie Ihre Plugin-APIs, um Breaking Changes zu verwalten
Häufige Fallstricke, die vermieden werden sollten:
- Zu komplexe Tools: Teilen Sie komplexe Operationen in mehrere fokussierte Tools auf
- Fehlende Input-Validierung: Validieren Sie Tool-Inputs immer gegen das Schema
- Resource-Performance: Vermeiden Sie teure Operationen in Resource-Handlern
- Prompt-Mehrdeutigkeit: Machen Sie Prompts spezifisch und umsetzbar
- Fehler verschlucken: Propagieren Sie Fehler immer zum MCP-Client
- State-Leakage: Halten Sie Plugin-State isoliert und räumen Sie in destroy() auf
- Fehlende Beschreibungen: Jede Capability benötigt eine klare Beschreibung für das Modell
Fazit
MCP-Plugin-Systeme verwandeln Server in erweiterbare Plattformen, indem sie Tools, Resources und Prompts in modulare, wiederverwendbare Komponenten organisieren. Durch die Implementierung von ordnungsgemäßer Capability-Registrierung, Plugin-Lebenszyklus-Verwaltung und MCP-Protokollverarbeitung schaffen Sie eine Architektur, die mit Ihren Anforderungen wächst.
Die hier gezeigten Muster—Capability-Registries, dynamisches Laden und Protokollmethoden-Verarbeitung—bieten eine solide Grundlage für produktionsreife MCP-Server. Jedes Plugin wird zu einem eigenständigen Paket von KI-Capabilities, die Modelle entdecken und nutzen können.
Beginnen Sie mit einfachen Plugins, die einige Tools oder Resources bereitstellen, und erweitern Sie dann, wenn Sie die Bedürfnisse Ihrer Benutzer verstehen. Der Schlüssel liegt darin, klare Capability-Definitionen beizubehalten und das MCP-Protokoll die Entdeckung und den Aufruf handhaben zu lassen.
Bleiben Sie dran und genießen Sie jeden Schritt Ihrer Coding-Reise.
