Model Context Protocol (MCP): Die Zukunft der Tool-Nutzung

Model Context Protocol (MCP): Die Zukunft der Tool-Nutzung

Foto von Tiago Ferreira auf Unsplash

Stellen Sie sich vor, Ihre KI könnte nicht nur Text generieren, sondern auch Ihre Datenbank abfragen, E-Mails versenden, Code ausführen und mit beliebigen externen Systemen interagieren – alles über ein standardisiertes Protokoll. Genau das ermöglicht das Model Context Protocol (MCP).

In diesem Artikel tauchen wir tief in die Architektur von MCP ein und bauen gemeinsam einen funktionsfähigen MCP-Server, den Sie direkt in Claude Desktop oder anderen MCP-kompatiblen Clients nutzen können.

Was ist das Model Context Protocol?

MCP ist ein offenes Protokoll, das von Anthropic entwickelt wurde, um die Kommunikation zwischen KI-Modellen und externen Datenquellen oder Tools zu standardisieren. Es löst ein fundamentales Problem: Wie können LLMs sicher und konsistent mit der Außenwelt interagieren?

Das Problem vor MCP

Traditionell musste jede Integration individuell entwickelt werden:

Probleme:

  • Jedes Tool braucht eigene Integration
  • Keine Wiederverwendbarkeit
  • Inkonsistente Schnittstellen
  • Sicherheitsrisiken durch Ad-hoc-Lösungen

Die MCP-Lösung

MCP führt eine standardisierte Abstraktionsschicht ein:

Architektur im Detail

MCP basiert auf einer Client-Server-Architektur mit JSON-RPC 2.0 als Kommunikationsprotokoll.

Die drei Hauptkomponenten

1. MCP Host (Client)

Der Host ist die KI-Anwendung, die MCP-Server nutzt:

  • Claude Desktop: Anthropics Desktop-Anwendung
  • Claude Code: Die CLI für Entwickler
  • Eigene Anwendungen: Jede App, die das MCP-Protokoll implementiert

Der Host:

  • Initiiert Verbindungen zu MCP-Servern
  • Sendet Anfragen für Tools, Resources und Prompts
  • Verarbeitet Server-Responses
  • Managed den Lifecycle der Verbindungen

2. MCP Server

Ein Server stellt Capabilities bereit:

CapabilityBeschreibungBeispiel
ToolsAusführbare Funktionensearch_database, send_email
ResourcesLesbare DatenquellenDateien, Datenbank-Schemas, APIs
PromptsVordefinierte Prompt-TemplatesAnalyse-Workflows, Report-Generierung

3. Transport Layer

MCP unterstützt verschiedene Transport-Mechanismen:

// stdio - Für lokale Prozesse
{
  "command": "python",
  "args": ["server.py"],
  "transport": "stdio"
}
 
// SSE - Für Remote-Server
{
  "url": "https://api.example.com/mcp",
  "transport": "sse"
}

Der Verbindungs-Lifecycle

Einen MCP-Server bauen

Jetzt wird es praktisch: Wir bauen einen MCP-Server, der Zugriff auf eine SQLite-Datenbank bietet.

Projektstruktur

mcp-sqlite-server/
├── src/
│   └── index.ts
├── package.json
├── tsconfig.json
└── README.md

Setup

mkdir mcp-sqlite-server && cd mcp-sqlite-server
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3

Der Server-Code

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import Database from "better-sqlite3";
 
// Datenbank initialisieren
const db = new Database("./data.db");
 
// Server erstellen
const server = new Server(
  {
    name: "sqlite-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);
 
// Tools definieren
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "query",
      description: "Führt eine SELECT-Abfrage auf der Datenbank aus",
      inputSchema: {
        type: "object",
        properties: {
          sql: {
            type: "string",
            description: "Die SQL SELECT-Abfrage",
          },
        },
        required: ["sql"],
      },
    },
    {
      name: "execute",
      description: "Führt eine INSERT/UPDATE/DELETE-Abfrage aus",
      inputSchema: {
        type: "object",
        properties: {
          sql: {
            type: "string",
            description: "Die SQL-Anweisung",
          },
          params: {
            type: "array",
            description: "Parameter für Prepared Statements",
            items: { type: "string" },
          },
        },
        required: ["sql"],
      },
    },
  ],
}));
 
// Tool-Ausführung
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
 
  try {
    switch (name) {
      case "query": {
        // Nur SELECT erlauben
        const sql = args?.sql as string;
        if (!sql.trim().toUpperCase().startsWith("SELECT")) {
          throw new Error("Nur SELECT-Abfragen erlaubt");
        }
        const rows = db.prepare(sql).all();
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(rows, null, 2),
            },
          ],
        };
      }
 
      case "execute": {
        const sql = args?.sql as string;
        const params = (args?.params as string[]) || [];
 
        // Gefährliche Operationen verhindern
        const upperSql = sql.trim().toUpperCase();
        if (upperSql.startsWith("DROP") || upperSql.startsWith("TRUNCATE")) {
          throw new Error("DROP und TRUNCATE sind nicht erlaubt");
        }
 
        const result = db.prepare(sql).run(...params);
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                changes: result.changes,
                lastInsertRowid: result.lastInsertRowid,
              }),
            },
          ],
        };
      }
 
      default:
        throw new Error(`Unbekanntes Tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Fehler: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
});
 
// Resources: Datenbank-Schema bereitstellen
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "sqlite://schema",
      name: "Datenbank-Schema",
      description: "Das Schema aller Tabellen in der Datenbank",
      mimeType: "application/json",
    },
  ],
}));
 
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;
 
  if (uri === "sqlite://schema") {
    const tables = db
      .prepare(
        `SELECT name, sql FROM sqlite_master
         WHERE type='table' AND name NOT LIKE 'sqlite_%'`
      )
      .all();
 
    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(tables, null, 2),
        },
      ],
    };
  }
 
  throw new Error(`Unbekannte Resource: ${uri}`);
});
 
// Server starten
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("SQLite MCP Server gestartet");
}
 
main().catch(console.error);

TypeScript-Konfiguration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Build und Test

# Kompilieren
npx tsc
 
# Testen (manuell)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}' | node dist/index.js

Integration in Claude Desktop

Nach dem Build können Sie den Server in Claude Desktop registrieren:

// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%/Claude/claude_desktop_config.json (Windows)
{
  "mcpServers": {
    "sqlite": {
      "command": "node",
      "args": ["/pfad/zu/mcp-sqlite-server/dist/index.js"],
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}

Nach dem Neustart von Claude Desktop steht der Server zur Verfügung.

Best Practices für MCP-Server

1. Sicherheit

// Input-Validierung
function validateSql(sql: string): boolean {
  const forbidden = ["DROP", "TRUNCATE", "DELETE FROM", "UPDATE"];
  const upperSql = sql.toUpperCase();
  return !forbidden.some((keyword) => upperSql.includes(keyword));
}
 
// Prepared Statements verwenden
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const user = stmt.get(userId);

2. Error Handling

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    // Tool-Logik
    return { content: [{ type: "text", text: result }] };
  } catch (error) {
    // Strukturierte Fehler zurückgeben
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            error: true,
            message: error.message,
            code: error.code || "UNKNOWN",
          }),
        },
      ],
      isError: true,
    };
  }
});

3. Logging

import { Logger } from "./logger";
 
const logger = new Logger("mcp-sqlite");
 
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  logger.info("Tool aufgerufen", {
    tool: request.params.name,
    args: request.params.arguments,
  });
 
  // ... Tool-Logik
 
  logger.debug("Tool abgeschlossen", { duration: Date.now() - start });
});

4. Dokumentation

Jedes Tool sollte ausführlich dokumentiert sein:

{
  name: "query",
  description: `
    Führt eine SELECT-Abfrage auf der SQLite-Datenbank aus.
 
    Beispiele:
    - "SELECT * FROM users WHERE active = 1"
    - "SELECT name, email FROM users LIMIT 10"
 
    Einschränkungen:
    - Nur SELECT-Abfragen erlaubt
    - Maximal 1000 Ergebnisse
  `,
  inputSchema: {
    type: "object",
    properties: {
      sql: {
        type: "string",
        description: "Eine gültige SQL SELECT-Abfrage",
        examples: ["SELECT * FROM users", "SELECT COUNT(*) FROM orders"],
      },
    },
    required: ["sql"],
  },
}

Fortgeschrittene Patterns

Streaming für große Datenmengen

// Für große Result-Sets: Pagination implementieren
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { offset = 0, limit = 100 } = request.params.arguments || {};
 
  const rows = db
    .prepare(`SELECT * FROM large_table LIMIT ? OFFSET ?`)
    .all(limit, offset);
 
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify({
          data: rows,
          pagination: {
            offset,
            limit,
            hasMore: rows.length === limit,
          },
        }),
      },
    ],
  };
});

Prompt-Templates

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [
    {
      name: "analyze-table",
      description: "Analysiert die Struktur und Daten einer Tabelle",
      arguments: [
        {
          name: "tableName",
          description: "Name der zu analysierenden Tabelle",
          required: true,
        },
      ],
    },
  ],
}));
 
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
 
  if (name === "analyze-table") {
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Analysiere die Tabelle "${args?.tableName}":
 
1. Zeige das Schema der Tabelle
2. Wie viele Einträge gibt es?
3. Welche Spalten haben NULL-Werte?
4. Gibt es Duplikate?
 
Nutze die verfügbaren Tools um diese Fragen zu beantworten.`,
          },
        },
      ],
    };
  }
 
  throw new Error(`Unbekannter Prompt: ${name}`);
});

Debugging und Entwicklung

MCP Inspector

Anthropic bietet einen Inspector für die Entwicklung:

npx @modelcontextprotocol/inspector node dist/index.js

Dieser öffnet eine Web-UI zum Testen Ihrer Tools und Resources.

Logging während der Entwicklung

// Alle JSON-RPC Nachrichten loggen
process.stdin.on("data", (data) => {
  console.error("← Eingehend:", data.toString());
});
 
const originalWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = (chunk: any, ...args: any[]) => {
  console.error("→ Ausgehend:", chunk.toString());
  return originalWrite(chunk, ...args);
};

Ausblick

MCP ist noch jung, aber die Entwicklung schreitet schnell voran:

  • Mehr Transports: WebSocket-Support, gRPC
  • Authentifizierung: OAuth, API-Keys auf Protokollebene
  • Ecosystem: Wachsende Bibliothek von Community-Servern
  • IDE-Integration: VS Code Extensions, JetBrains Plugins

Das Protokoll hat das Potenzial, zum Standard für KI-Tool-Integration zu werden – ähnlich wie HTTP für Web-Kommunikation.

Fazit

MCP bietet eine elegante Lösung für das Problem der KI-Tool-Integration:

  • Standardisierung: Ein Protokoll für alle Tools
  • Sicherheit: Klare Grenzen zwischen Client und Server
  • Wiederverwendbarkeit: Ein Server, viele Clients
  • Erweiterbarkeit: Einfach neue Capabilities hinzufügen

Mit dem Wissen aus diesem Artikel können Sie:

  1. Die MCP-Architektur verstehen und erklären
  2. Eigene MCP-Server für Ihre Anwendungsfälle bauen
  3. Bestehende Server evaluieren und integrieren

Interesse an einer MCP-Integration für Ihr Unternehmen? Kontaktieren Sie mich für eine technische Beratung.

Model Context Protocol (MCP): Die Zukunft der Tool-Nutzung - Evangelos Dimitriadis