MCPTypeScriptGitHub CopilotVS CodeAI

Einen eigenen MCP-Server mit TypeScript bauen und GitHub Copilot erweitern

Sloth255
Sloth255
·10 min read·2,145 words

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 Antwort

Kommunikation 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| G

Die 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 enum verwenden
  • required ausdrücklich deklarieren
  • Mit additionalProperties: false unbeabsichtigte 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:

  1. Unit-Tests für die Eingabevalidierung
  2. Unit-Tests für Tool-Handler
  3. Integrationstests einschließlich Transport
  4. 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:

  1. Mit stdio einen rein lokalen Server erstellen
  2. Nur ein oder zwei schreibgeschützte Tools erstellen
  3. Ressourcen und Prompts ergänzen
  4. Logging und Fehlerbehandlung verbessern
  5. 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/server erleichtert 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.json registrierter 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 inputSchema verbessert 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.

Referenzen