Einführung
MCP (Model Context Protocol) ist ein von Anthropic entwickeltes Standardprotokoll, das KI-Modelle mit externen Tools und Datenquellen verbindet. Es wird von wichtigen KI-Anwendungen wie GitHub Copilot, Claude und Cursor unterstützt.
Ein eigener MCP-Server eignet sich etwa, um eine interne API aus Copilot heraus aufzurufen oder eine KI projektspezifische Befehle ausführen zu lassen.
Dieser Artikel zeigt, wie sich ein MCP-Server mit TypeScript implementieren und an GitHub Copilot in VS Code anbinden lässt. Er behandelt Einrichtung, Design und bewährte Betriebspraktiken.
MCP-Grundlagen
flowchart TD
A[VS Code Copilot Agent] -->|MCP| B[MCP Server]
B --> C[Externe Tools]
B --> D[API]
B --> E[Datenbank]Ein MCP-Server kann drei zentrale Arten von Funktionen bereitstellen.
| Kategorie | Beschreibung | Beispiele |
|---|---|---|
| Tools | Funktionen, die das LLM aufrufen kann | API-Aufrufe, Dateioperationen |
| Ressourcen | Daten, die gelesen werden können | Dokumente, Konfigurationsdateien |
| Prompts | Wiederverwendbare Prompt-Vorlagen | Code-Review, Incident-Untersuchung |
Die Kommunikation verwendet JSON-RPC über stdio oder Streamable HTTP.
MCP-Design und Architektur
Warum MCP entstanden ist
Vor MCP implementierte jeder KI-Client eigene Anbindungen an externe Systeme. Sollte dieselbe interne API von mehreren KI-Tools genutzt werden, war für jeden KI-Client eine separate Integration nötig.
[Vor MCP]
GitHub Copilot -> Eigene Integration A -> Interne API
Claude -> Eigene Integration B -> Interne API (doppelt)
Cursor -> Eigene Integration C -> Interne API (doppelt)
[Mit MCP]
GitHub Copilot -+
Claude +-> MCP-Server -> Interne API (eine Implementierung)
Cursor -+
MCP löst diese Fragmentierung mit einem Standardprotokoll. Nach der Implementierung eines Servers muss dieselbe Funktion nicht mehr für jeden Client erneut erstellt werden. Welche Funktionen tatsächlich verfügbar sind, hängt weiterhin von der MCP-Version, dem Transport und den Authentifizierungsverfahren ab, die der jeweilige KI-Client unterstützt.
Das Kernprinzip: Das LLM entscheidet, der Server führt aus
Das zentrale MCP-Designprinzip ist die Trennung der Verantwortlichkeiten.
- LLM (z. B. Copilot): Versteht den Kontext und entscheidet, welches Tool mit welchen Argumenten aufgerufen wird.
- MCP-Server: Führt das Tool aus und gibt das Ergebnis zurück.
Ein LLM greift nicht direkt auf externe Systeme zu. Jede Anfrage durchläuft eine explizite Tool-Call-Schnittstelle. So lassen sich Authentifizierung, Validierung und Protokollierung auf dem Server zentral verwalten.
sequenceDiagram
participant U as Benutzer
participant L as LLM (Copilot)
participant S as MCP-Server
participant A as Externe API
U->>L: "Prüfe offene Issues"
L->>S: tools/call search_github_issues
S->>A: GET /search/issues
A-->>S: Ergebnis-JSON
S-->>L: Ergebnis der Tool-Ausführung
L-->>U: Aufbereitete AntwortKommunikation mit JSON-RPC 2.0
Die Kommunikation erfolgt über JSON-RPC 2.0. Nach dem Verbindungsaufbau durchläuft der Nachrichtenfluss diese drei Schritte.
| Schritt | Methode | Beschreibung |
|---|---|---|
| 1 | initialize |
Client und Server tauschen Versionen und unterstützte Funktionen aus |
| 2 | tools/list |
Der Client ruft die Liste der Tools mit Namen und Schemata ab |
| 3 | tools/call |
Der Client fordert die Ausführung eines Tools an |
Im Folgenden ein Beispiel für Anfrage und Antwort zu search_github_issues:
// Anfrage
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_github_issues",
"arguments": {
"query": "authentication",
"repo": "owner/my-repo",
"state": "open"
}
}
}
// Antwort
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "[{\"number\": 42, \"title\": \"Fix OAuth token refresh\", ...}]"
}
]
}
}
Das LLM erhält diesen content, formuliert daraus eine Antwort in natürlicher Sprache und gibt sie an den Benutzer zurück.
Die Rolle des Transports
MCP definiert zwei Standardtransporte: stdio und Streamable HTTP. Der ältere Transport HTTP+SSE bleibt aus Gründen der Abwärtskompatibilität erhalten; für neue Implementierungen empfiehlt sich jedoch Streamable HTTP.
| stdio | Streamable HTTP | |
|---|---|---|
| Funktionsweise | Startet den Server als Unterprozess und kommuniziert über Standardein- und -ausgabe | Sendet und empfängt JSON-RPC über HTTP POST/GET und streamt bei Bedarf über SSE |
| Geeignet für | Lokale Entwicklung und individuelle Nutzung | Teamweite Nutzung, zentrale Verwaltung, mehrere Clients |
| Startaufwand | Gering; kann bei jedem Aufruf gestartet werden | Setzt einen dauerhaft laufenden Dienst voraus |
Für die lokale Nutzung mit VS Code ist stdio die übliche Wahl. Der Abschnitt „Auswahl von stdio oder Streamable HTTP“ greift diese Entscheidung später noch einmal auf.
Wann MCP statt Skills sinnvoll ist
GitHub Copilot in VS Code bietet zwei Mechanismen, um sein Verhalten über Markdown-Dateien zu erweitern. Benutzerdefinierte Anweisungsdateien wie .github/copilot-instructions.md und .instructions.md definieren Leitlinien für die KI. Agent-Skills (SKILL.md) beschreiben wiederverwendbare Abläufe, die Verfahren, Skripte und ergänzende Ressourcen enthalten können. Dieser Artikel fasst beide als „anweisungsbasierte Anpassung“ zusammen. Vor dem Aufbau eines MCP-Servers sollte geprüft werden, ob diese Mechanismen ausreichen.
Anwendungsfälle für anweisungsbasierte Anpassungen
- Der KI sollen statische Informationen wie Codierungskonventionen oder die Projektstruktur bereitgestellt werden.
- Ein etabliertes Verfahren soll als Prompt-Vorlage wiederverwendet werden.
- Es soll ein Workflow definiert werden, der keinen Zugriff auf externe Systeme benötigt.
Anweisungsbasierte Anpassungen benötigen keinen dedizierten Server. Ein Agent kann Skripte eines Skills über vorhandene Tools wie das Terminal ausführen. Ob das möglich ist, welche Freigaben erforderlich sind und in welchem Format Ergebnisse zurückkommen, hängt jedoch von den Fähigkeiten der Hostumgebung ab. Daher sollte diese Option zuerst geprüft werden.
Skills können auch API-Aufrufe dokumentieren
Bei der Anforderung „Eine externe API verwenden“ kann der Aufruf in Skills dokumentiert und Copilot zur Generierung von Code oder eines curl-Befehls verwendet werden.
<!-- Beispiel für SKILL.md -->
Der Endpunkt der internen Status-API lautet https://api.internal/status.
Die Authentifizierung erfolgt mit einem Bearer-Token im Authorization-Header.
Beispielaufruf mit curl: curl -H "Authorization: Bearer $TOKEN" https://api.internal/status
Dieser Ansatz eignet sich, wenn Copilot Code generieren soll. Kann ein Agent eine API über ein vorhandenes Terminal oder eine Erweiterung aufrufen, hängen Ausführungsverfahren, Freigabe und Verarbeitung der Ergebnisse weiterhin von der Hostumgebung ab.
Wann MCP erforderlich wird
Skills reichen nicht mehr aus, sobald einer der folgenden Punkte zutrifft.
| Situation | Anweisungsbasierte Anpassung | MCP |
|---|---|---|
| Copilot vermitteln, wie eine API aufgerufen wird | Ja | — |
| Einen API-Ausführungsablauf mit Tools der Hostumgebung definieren | Ja | — |
| Eine API als typisiertes, dediziertes Tool bereitstellen | Nein | Ja |
| Authentifizierung, Eingabevalidierung und Ergebnisformat auf dem Server vereinheitlichen | Nein | Ja |
| Dieselbe Funktion mehreren MCP-Clients bereitstellen | Nein | Ja |
Skills vermitteln einem Agenten, „was in welcher Reihenfolge zu tun ist“. MCP stellt hingegen einen „typisierten, dedizierten Tool-Vertrag“ bereit, den Clients aufrufen können. Bei MCP führt der Server Aufrufe aus und liefert Ergebnisse in einem definierten Format in den Kontext von Copilot zurück. Entscheidend sind dabei Kontrolle und Portabilität.
Anwendungsfälle für einen MCP-Server
Ein eigener MCP-Server ist sinnvoll, wenn sich Anforderungen wie die folgenden allein mit anweisungsbasierten Anpassungen nur schwer erfüllen lassen.
| Anforderung | Grund |
|---|---|
| API-Ergebnisse für Copilots Schlussfolgerungen nutzen | Ausführungsergebnisse gelangen direkt in den Kontext und ermöglichen Folgefragen sowie Transformationen |
| Auf authentifizierte Ressourcen zugreifen | Tokens und Zugangsdaten lassen sich sicher in Umgebungsvariablen speichern, ohne sie Benutzern offenzulegen |
| Schreib- oder Aktualisierungsvorgänge ausführen | Operationen mit Nebenwirkungen, etwa Issues erstellen, Labels aktualisieren oder Datensätze ändern |
| Echtzeitdaten benötigen | Aktuellen CI-Status oder neueste Datenbankwerte abrufen, die nicht in statischen Dateien stehen können |
| Große oder dynamische Daten verarbeiten | Dokumente, die zu groß für Prompts sind, oder Daten, die sich häufig ändern |
| Komplexe Verarbeitung oder Umwandlung durchführen | Dateianalyse, Aggregation und Formatkonvertierung, die im Code erfolgen sollen |
| Projekt- oder teamübergreifend teilen | Tools zentral verwalten, die mehrere Entwickler und Repositories aufrufen |
Entscheidungsfluss
flowchart TD
A[Es gibt eine Aufgabe] --> B{Ist Zugriff auf ein<br/>externes System nötig?}
B -->|Nein| C[Anweisungsbasierte Anpassung genügt]
B -->|Ja| D{Muss Copilot das Ergebnis<br/>erhalten und damit arbeiten?}
D -->|Nein - Codegenerierung genügt| C
D -->|Ja| E{Sind Authentifizierung, Nebenwirkungen<br/>oder dynamische Daten beteiligt?}
E -->|Nein| F[Prüfen, ob ein vorhandener<br/>MCP-Server genügt]
E -->|Ja| G[MCP-Server erstellen]
F -->|Falls keiner existiert| GDie zentrale Abgrenzung zwischen anweisungsbasierter Anpassung und MCP lautet: Müssen ein dedizierter Tool-Vertrag, Authentifizierung und Eingabevalidierung auf dem Server verwaltet werden?
Einrichtung
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node tsx
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true
},
"include": ["src/**/*"]
}
// package.json (Auszug)
{
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Implementierung eines einfachen MCP-Servers
// src/index.ts
import { McpServer } from '@modelcontextprotocol/server'
import { serveStdio } from '@modelcontextprotocol/server/stdio'
import * as z from 'zod/v4'
import { fetchWeather } from './tools/weather.js'
interface GithubIssueSearchResponse {
items: Array<{
number: number
title: string
state: string
html_url: string
}>
}
function createServer(): McpServer {
const server = new McpServer({
name: 'my-custom-tools',
version: '1.0.0',
})
server.registerTool(
'get_weather',
{
description: 'Ruft das aktuelle Wetter für die angegebene Stadt ab',
inputSchema: z.object({
city: z.string().describe('Name der Stadt, für die das Wetter abgefragt wird (z. B. Tokyo, Osaka)'),
}),
},
async ({ city }) => {
const weather = await fetchWeather(city)
return {
content: [{
type: 'text',
text: `Aktuelles Wetter in ${city}: ${weather.description}, Temperatur: ${weather.temp} °C`,
}],
}
}
)
server.registerTool(
'search_github_issues',
{
description: 'Durchsucht Issues in einem GitHub-Repository',
inputSchema: z.object({
query: z.string().describe('Suchanfrage'),
repo: z.string().regex(/^[^/]+\/[^/]+$/).describe('Repository-Name (Format owner/repo)'),
state: z.enum(['open', 'closed', 'all']).default('open').describe('Issue-Status'),
}),
},
async ({ query, repo, state }) => {
const token = process.env.GITHUB_TOKEN
if (!token) {
return {
content: [{ type: 'text', text: 'GITHUB_TOKEN ist nicht konfiguriert' }],
isError: true,
}
}
const stateQualifier = state === 'all' ? '' : ` state:${state}`
const url = new URL('https://api.github.com/search/issues')
url.searchParams.set(
'q',
`${query} is:issue repo:${repo}${stateQualifier}`
)
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
})
if (!response.ok) {
return {
content: [{ type: 'text', text: `GitHub API error: ${response.status}` }],
isError: true,
}
}
const data = await response.json() as GithubIssueSearchResponse
const issues = data.items.slice(0, 5).map((issue) => ({
number: issue.number,
title: issue.title,
state: issue.state,
url: issue.html_url,
}))
return {
content: [{ type: 'text', text: JSON.stringify(issues, null, 2) }],
}
}
)
return server
}
void serveStdio(createServer)
console.error('MCP-Server wurde gestartet')
Praxisbeispiele für Tool-Implementierungen
Wetter-API (externer API-Aufruf)
// src/tools/weather.ts
interface WeatherData {
description: string
temp: number
humidity: number
}
export async function fetchWeather(city: string): Promise<WeatherData> {
const apiKey = process.env.OPENWEATHER_API_KEY
if (!apiKey) throw new Error('OPENWEATHER_API_KEY ist nicht konfiguriert')
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric&lang=ja`
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Abruf der Wetterdaten fehlgeschlagen: ${response.statusText}`)
}
const data = await response.json()
return {
description: data.weather[0].description,
temp: Math.round(data.main.temp),
humidity: data.main.humidity,
}
}
Interne Dokumentensuche (lokale Dateien)
// src/tools/docs-search.ts
import * as fs from 'fs/promises'
import * as path from 'path'
import { pathToFileURL } from 'url'
interface DocResult {
title: string
excerpt: string
url: string
}
export async function searchDocs(
query: string,
limit: number
): Promise<DocResult[]> {
const docsDir = process.env.DOCS_DIR ?? './docs'
const files = await fs.readdir(docsDir)
const results: DocResult[] = []
for (const file of files.filter((f) => f.endsWith('.md'))) {
const content = await fs.readFile(path.join(docsDir, file), 'utf-8')
if (content.toLowerCase().includes(query.toLowerCase())) {
const lines = content.split('\n')
// Frontmatter (erster --- Block) überspringen und erste H1 als Titel verwenden
let bodyStart = 0
if (lines[0]?.trim() === '---') {
const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---')
bodyStart = end >= 0 ? end + 1 : 0
}
const h1 = lines.slice(bodyStart).find((l) => /^#\s/.test(l))
const title = h1 ? h1.replace(/^#\s*/, '') : path.basename(file, '.md')
const matchIndex = content.toLowerCase().indexOf(query.toLowerCase())
const excerpt = content.slice(
Math.max(0, matchIndex - 50),
matchIndex + 150
)
results.push({
title,
excerpt,
url: pathToFileURL(path.resolve(docsDir, file)).toString(),
})
if (results.length >= limit) break
}
}
return results
}
Ressourcen implementieren
Ressourcen stellen schreibgeschützte Daten bereit, auf die eine KI verweisen kann. Sie eignen sich für Informationen, die das Modell zuverlässig abrufen soll, etwa interne Richtlinien, Betriebsabläufe und API-Dokumentation.
// Am Anfang von src/index.ts ergänzen
import { readFile } from 'node:fs/promises'
import path from 'node:path'
// Vor return server in createServer ergänzen
server.registerResource(
'project-guide',
'docs://project-guide',
{
title: 'Projektleitfaden',
description: 'Ein Projektleitfaden als Referenz während der Entwicklung',
mimeType: 'text/markdown',
},
async (uri) => {
const docsDir = process.env.DOCS_DIR ?? path.join(process.cwd(), 'docs')
const content = await readFile(
path.join(docsDir, 'project-guide.md'),
'utf-8'
)
return {
contents: [
{
uri: uri.href,
mimeType: 'text/markdown',
text: content,
},
],
}
}
)
Geeignete Ressourcen sind etwa:
- Designleitfäden und Codierungskonventionen
- Operative Runbooks
- Zusammenfassungen von API-Spezifikationen
- Interne FAQs
Der wesentliche Vorteil: Diese Informationen müssen nicht in jeden Chat eingefügt werden, um referenziert werden zu können.
Prompts verwenden
MCP kann neben Tools auch Prompts bereitstellen. Sie eignen sich für wiederkehrende Verfahren oder Prüfungsperspektiven.
- Prompt für Sicherheitsüberprüfungen
- Prompt zum Erstellen von PR-Beschreibungen
- Prompt für die erste Incident-Untersuchung
Tools lassen sich als „Ausführungsmittel“, Prompts als „Denkmuster“ verstehen. Diese Unterscheidung hilft dabei, sinnvolle Einsatzfälle zu erkennen.
Mit VS Code verbinden
// .vscode/mcp.json
{
"servers": {
"my-custom-tools": {
"type": "stdio",
"command": "node",
"args": ["${workspaceFolder}/dist/index.js"],
"env": {
"GITHUB_TOKEN": "${env:GITHUB_TOKEN}",
"OPENWEATHER_API_KEY": "${env:OPENWEATHER_API_KEY}"
}
}
}
}
Während der Entwicklung kann der Server direkt mit tsx ausgeführt werden; damit entfällt ein separater Build-Schritt.
{
"servers": {
"my-custom-tools-dev": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "${workspaceFolder}/src/index.ts"]
}
}
}
Verwendung im Copilot-Chat
Nach einem Neustart von VS Code stehen die MCP-Tools im Copilot-Chat zur Verfügung.
"Wie ist das Wetter in Tokio?"
-> Copilot ruft automatisch das Tool get_weather auf
"Finde offene Issues zu \"memory leak\""
-> Copilot ruft automatisch das Tool search_github_issues auf
Tools lassen sich auch nach Servernamen filtern.
#my-custom-tools Liste offene Issues zu "authentication" in owner/my-repo auf
-> Ruft das Tool search_github_issues über diesen Server auf
Debugging-Tipps
# Tools mit MCP Inspector interaktiv testen
npx @modelcontextprotocol/inspector npx tsx src/index.ts
Die vom Inspector angezeigte URL im Browser öffnen, um Tool-Aufrufe und Rückgabewerte visuell zu prüfen.
Best Practices für das Tool-Design
Zuerst festlegen, was nicht erlaubt sein soll
MCP-Tools sind nützlich, doch ein Tool mit zu weitreichenden Funktionen ist riskant.
| Schlechtes Beispiel | Gutes Beispiel |
|---|---|
manage_project |
list_open_issues, update_issue_labels |
run_anything |
get_pipeline_status |
execute_sql |
search_customers |
Tools sind zuverlässiger, wenn mögliche Nebenwirkungen aus ihrem Namen hervorgehen, sie wenige Eingabeparameter haben und sich ihr Fehlerverhalten leicht erklären lässt.
inputSchema sorgfältig gestalten
Ein Eingabeschema für Tools dient nicht nur der Validierung, sondern ist auch eine Schnittstelle zum LLM. Vage Beschreibungen verringern die Genauigkeit bei der Ableitung von Argumenten.
const updateIssueInput = {
type: 'object',
properties: {
repo: { type: 'string', pattern: '^[^/]+/[^/]+$' },
issueNumber: { type: 'integer', minimum: 1 },
labels: {
type: 'array',
items: { type: 'string' },
maxItems: 10,
},
},
required: ['repo', 'issueNumber', 'labels'],
additionalProperties: false,
}
Wesentliche Punkte:
- Konkrete
description-Werte schreiben - Wo möglich
enumverwenden requiredausdrücklich deklarieren- Mit
additionalProperties: falseunbeabsichtigte Parameter verhindern
Verantwortlichkeiten von Zod und JSON Schema trennen
inputSchema ist ein externer Vertrag, Zod übernimmt die erneute Laufzeitvalidierung. Insbesondere bei Tools, die eine externe API oder Datenbank aufrufen, darf die serverseitige Validierung nicht ausgelassen werden.
case 'update_issue': {
// Form für das Modell im JSON Schema definieren
// und die tatsächlich akzeptierten Werte mit Zod validieren
const input = z.object({
repo: z.string().regex(/^[^/]+\/[^/]+$/),
issueNumber: z.number().int().positive(),
labels: z.array(z.string()).max(10),
}).parse(args)
// ...
}
Grundsätze der Fehlerbehandlung
Fehler lassen sich leichter behandeln, wenn sie mindestens in diese drei Kategorien unterteilt werden:
- Eingabefehler (Validierungsfehler)
- Authentifizierungs-/Autorisierungsfehler
- Ausfälle externer Dienste
function toToolErrorMessage(error: unknown): string {
if (error instanceof z.ZodError) {
return 'Ungültige Eingabe. Pflichtfelder und Werteformate prüfen.'
}
if (error instanceof Error) {
return `Tool-Ausführung fehlgeschlagen: ${error.message}`
}
return 'Tool-Ausführung fehlgeschlagen.'
}
Zurückgegebene Meldungen sollten keine unnötigen internen Details preisgeben und den nächsten Schritt klar benennen.
Umgang mit Zugangsdaten
Geheimnisse dürfen niemals über Prompts oder Argumente übergeben werden; sie gehören in Umgebungsvariablen oder eine sichere Ausführungsumgebung.
| Tun | Nicht |
|---|---|
| API-Tokens aus env lesen | Geheimnisse in Tool-Argumente aufnehmen |
| Tokens mit den geringstmöglichen Rechten verwenden | Tokens in Protokolle schreiben |
| Produktions- und Entwicklungsumgebungen trennen | Geheimnisse in Fehlermeldungen aufnehmen |
Berechtigungen minimal halten
- Für die Suche nach GitHub-Issues ein schreibgeschütztes Token verwenden
- Für ein Datenbank-Lesetool einen Benutzer mit ausschließlich SELECT-Berechtigung verwenden
- Einem Tool zur Deployment-Prüfung ausschließlich Leseberechtigung für Workflows geben
Für die Teamnutzung ist es sicherer, schreibgeschützte Tools, Update-Tools und produktionsnahe Tools voneinander zu trennen.
Betriebsdesign
Timeouts, Wiederholungsversuche und Teilfehler
Ein lokales Experiment kann zuverlässig funktionieren, während der Betrieb in Produktion instabil ist. Code sollte daher nicht unter der Annahme geschrieben werden, dass jeder Aufruf erfolgreich sein wird.
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return await Promise.race([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
}),
])
}
Zu den Mindestmaßnahmen gehören:
- Timeouts
- Obergrenzen für Wiederholungsversuche
- Rate Limiting
- Normalisierung von Fehlermeldungen
Beobachtbarkeit (Logging-Design)
Beim Debuggen genügt es nicht, nur zu wissen, was aufgerufen wurde. Diese Angaben sollten protokolliert werden:
- Name des Tools
- Aufrufzeitpunkt und Ausführungsdauer
- Kategorie für Erfolg oder Fehler sowie Fehlergrund
Es darf jedoch nicht zu viel protokolliert werden: Personenbezogene Daten, Zugriffstokens, vollständige SQL-Anweisungen und komplette Antworten externer APIs gehören nicht in die Logs.
Teststrategie
Durch die Auslagerung von Tool-Implementierungen in Funktionen lässt sich ihre Logik unabhängig vom MCP-Protokoll testen.
// Geschäftslogik auslagern, damit sie testbar bleibt
export async function searchGithubIssues(input: SearchIssuesInput) {
// Tatsächliche Verarbeitung
}
server.registerTool(
'search_github_issues',
{ inputSchema: SearchIssuesInputSchema },
async (input) => formatResult(await searchGithubIssues(input))
)
Empfohlene Testebenen:
- Unit-Tests für die Eingabevalidierung
- Unit-Tests für Tool-Handler
- Integrationstests einschließlich Transport
- Manuelle Prüfung mit MCP Inspector
Auswahl von stdio oder Streamable HTTP
| Perspektive | stdio | Streamable HTTP |
|---|---|---|
| Lokale Einrichtung | Sehr einfach | Etwas aufwendiger |
| Zentrale Kontrolle | Schwach | Stark |
| Authentifizierungs-Governance | Abhängig von einzelnen Benutzern | Einfacher |
| Debugging | Einfach | Zusätzliche Netzwerkfaktoren |
| Bereitstellung | npm / Binärdatei | Dienst-URL |
Zunächst sollte die Nutzbarkeit mit stdio geprüft werden. Sobald teamweite Nutzung oder zentrale Verwaltung erforderlich werden, ist Streamable HTTP eine Option. Bei einer Bereitstellung über HTTP müssen Origin-Validierung, Bindung an localhost und Authentifizierung implementiert werden.
MCP-Server aufteilen
Wenn ein Server alle Funktionen bündelt, wächst sein Verantwortungsbereich schnell. Eine Aufteilung nach fachlichen Einheiten erleichtert den Betrieb.
- GitHub-bezogene Tools
- Tools für interne APIs
- Tools zum Lesen von Datenbanken
- Server, der ausschließlich Dokumentationsressourcen bereitstellt
Durch die Aufteilung lassen sich außerdem Berechtigungen und Umgebungsvariablen leichter organisieren.
Häufige Fehlermuster
1. Ein Allzweck-Tool entwickeln, das alles kann
Designs wie run_anything oder execute_sql für beliebiges SQL verringern häufig sowohl Sicherheit als auch Reproduzierbarkeit.
2. inputSchema zu offen gestalten
Das Modell kann Argumente dann schlechter zuverlässig zusammensetzen, was zu mehr fehlgeschlagenen Aufrufen führt.
3. Geheimnisse als Argumente entgegennehmen
Sie können leicht in Logs oder Konversationsverläufe gelangen.
4. Lese- und Aktualisierungsvorgänge unter derselben Berechtigung vermischen
Dadurch steigt das Risiko betrieblicher Fehler.
5. Tool-Implementierungen direkt in Handler schreiben
Das erschwert Tests und Wartung.
Eine praxisnahe Einführungsreihenfolge
Für den ersten MCP-Server ist diese Reihenfolge praxisnah:
- Mit stdio einen rein lokalen Server erstellen
- Nur ein oder zwei schreibgeschützte Tools erstellen
- Ressourcen und Prompts ergänzen
- Logging und Fehlerbehandlung verbessern
- Aktualisierungs-Tools nur bei Bedarf vorsichtig ergänzen
Diese Reihenfolge erleichtert die Balance zwischen Sicherheit und Nutzen.
Geeignete erste Anwendungsfälle:
- Suche nach Projektkonfigurationen
- Suche nach internen Dokumenten
- CI-Statusprüfungen
- PR- und Issue-Suche
Aktualisierungs-Tools sollten erst ergänzt werden, wenn ein tragfähiges Vertrauens- und Prüfkonzept besteht.
Zusammenfassung
- MCP-Server kommunizieren über JSON-RPC via stdio oder Streamable HTTP; das aktuelle
@modelcontextprotocol/servererleichtert die Implementierung mit TypeScript. - Die Kombination aus Tools (Ausführung), Ressourcen (Referenzdaten) und Prompts (Denkmustern) erweitert die Fähigkeiten von Copilot.
- Ein in
.vscode/mcp.jsonregistrierter Server lässt sich aus GitHub Copilot in VS Code aufrufen. - Tool-Design beginnt mit der Frage, was nicht zulässig sein soll. Ein sorgfältig gestaltetes
inputSchemaverbessert die Genauigkeit, mit der das LLM Argumente ableitet. - Geheimnisse gehören in Umgebungsvariablen, Berechtigungen bleiben auf das notwendige Minimum beschränkt.
- Zunächst mit stdio und ein oder zwei schreibgeschützten Tools starten und erst nach bewährtem Einsatz erweitern.
