Mitte 2026 ist MCP (Model Context Protocol) der de-facto-Standard für die Verbindung von LLMs mit Tools. Anthropic führte es ein; OpenAI, Google und das breitere Ökosystem haben es übernommen. Cursor, Claude Desktop, ChatGPT, benutzerdefinierte Agenten — alle verwenden MCP.
Wenn LLM-Agenten mit Ihrem Service interagieren sollen, stellen Sie einen MCP-Server bereit. Nach ein oder zwei Implementierungen wird deutlich, wie kompakt das Protokoll selbst ist. Die anspruchsvolle Engineering-Arbeit liegt in den umgebenden Bereichen: Schemadesign, Fehlerbehandlung, Authentifizierung, Streaming, Performance und Observability.
Dieser Artikel ist ein tiefgehender Einblick in das Erstellen von produktionsreifen MCP-Servern in TypeScript. Wir behandeln die Muster, die unter realen Agenten-Nutzungsszenarien funktionieren, nicht nur die Protokollmechanik.
MCP in Kürze
MCP ist ein Client-Server-Protokoll, bei dem:
- Server Tools, Ressourcen und Prompts bereitstellen.
- Clients typischerweise LLM-Agenten sind, die sie nutzen.
Das Protokoll verwendet JSON-RPC 2.0 (die offizielle Spezifikation ist kurz und lohnt sich einmal zu lesen). Transporte sind stdio für lokale Prozesse und Streamable HTTP für entfernte Server — der ältere HTTP+SSE-Transport wurde in der Spezifikationsrevision vom 26. März 2025 veraltet, daher behandeln Sie alle Tutorials, die darauf basieren, als historisch. Authentifizierung und Sicherheit sind Protokollangelegenheiten; die wichtigsten Implementierungen unterstützen OAuth, API-Schlüssel und ähnliche Methoden.
Die Aufgabe des Servers besteht darin, nützliche Fähigkeiten für LLMs in einer Weise bereitzustellen, die sie entdecken und nutzen können.
Die grundlegende Struktur
Mit dem offiziellen Paket @modelcontextprotocol/sdk sieht ein minimaler Server über die High-Level-API McpServer folgendermaßen aus:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
server.registerTool(
"echo",
{
description: "Echo back the provided text.",
inputSchema: { text: z.string() },
},
async ({ text }) => ({
content: [{ type: "text", text }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
(Dasselbe SDK stellt auch die niedrigere Ebene Server-Klasse und setRequestHandler für ListToolsRequestSchema / CallToolRequestSchema bereit, falls Sie volle Kontrolle über die Request-Handler wünschen — aber für die meisten Server ist McpServer.registerTool kürzer und schwerer zu falsch implementieren.)
Das ist das Skelett. Was Sie in die Tool-Handler setzen — und wie — ist der eigentliche Arbeitsteil.
Muster 1: Tool-Design-Philosophie
Die erste Entscheidung: Welche Tools stellen Sie bereit und mit welcher Granularität?
Ein häufiger Fehler besteht darin, die zugrunde liegende API unmittelbar als Tools bereitzustellen. Bei 200 REST-Endpunkten wären 200 Tools ein Desaster: Modelle arbeiten mit zu vielen Tools schlechter, Toolbeschreibungen werden unübersichtlich und das Protokoll zum Labyrinth.
Besser: Designen Sie Tools nach der Art und Weise, wie Agenten sie nutzen möchten. Jedes Tool führt eine gut definierte Sache aus, nimmt gut definierte Eingaben entgegen und gibt gut definierte Ausgaben zurück.
Einige Prinzipien:
Ein Konzept pro Tool. Vermeiden Sie ein manage_customer-Tool, das 12 verschiedene Dinge tut. Nutzen Sie stattdessen search_customers, get_customer, update_customer_email, archive_customer — jedes mit Fokus.
Richtige Granularität. Zu granular und der Agent muss viele Aufrufe tätigen; zu grob und er kann nicht präzise das tun, was benötigt wird. Streben Sie nach „Operationen, die ein Mensch benennen würde“.
Aktionssätze. search_documents, nicht documents. Tools sollten nach dem beschreiben, was sie tun.
Unterscheidung zwischen Lesen und Schreiben. Lesetools sind sicherer; Schreibtools haben Nebenwirkungen. Unterscheiden Sie in der Benennung (list_x vs. create_x) und behandeln Sie sie anders (erfordern explizite Bestätigung, Idempotenzschlüssel usw.).
Aggregieren, wenn nützlich. Ein get_customer_profile, der Kunden + kürzliche Bestellungen + Supporttickets in einem Aufruf zurückgibt, ist oft besser als drei separate Aufrufe. Der Agent erhält den Kontext in einem Schritt.
Für einen Server, der beispielsweise ein Kundensupportsystem bereitstellt, sind 8–15 Tools eine sinnvolle Größenordnung. Mehr ist meist zu viel.
Muster 2: Schema-Design
Jedes Tool hat ein Eingabeschema (Parameter, die der LLM bereitstellen muss) und ein Ausgabeschema (was Ihr Tool zurückgibt). Schemata sind nicht nur für Validierung; sie sind Prompt-Engineering.
Mit Zod für Eingabeschemata:
const searchCustomersSchema = z.object({
query: z.string().describe(
"Search term: name, email, or company. Be specific to avoid too many matches."
),
limit: z.number().int().min(1).max(50).default(10).describe(
"Maximum results to return. Default 10, max 50."
),
filters: z.object({
tier: z.enum(["free", "pro", "enterprise"]).optional().describe(
"Filter to specific customer tier"
),
status: z.enum(["active", "trial", "churned"]).optional().describe(
"Filter by customer status"
),
}).optional(),
});
Achten Sie auf:
- Jedes Feld besitzt eine
.describe()-Beschreibung, die das LLM liest. - Enums sind explizit; frei formulierte Zeichenfolgen werden nach Möglichkeit eingeschränkt.
- Standardwerte sind sinnvoll.
- Einschränkungen (Min/Max, Länge) sind explizit.
- Ob verpflichtend oder optional ist klar.
Die Beschreibungen sind von entscheidender Bedeutung. „Suchbegriff“ ist unhilfreich; „Suchbegriff: Name, E-Mail oder Firma. Seien Sie präzise, um zu viele Übereinstimmungen zu vermeiden“ ist nützliche Anleitung für den LLM.
Muster 3: Ausgabestruktur
Die Ausgabe ist das, was der LLM sieht und darauf reagiert. Ein gutes Ausgabedesign verbessert dramatisch das Verhalten des LLM.
Strukturierte Ausgaben.
type SearchResult = {
customers: Customer[];
total_matches: number;
truncated: boolean;
next_page_cursor?: string;
};
Mit Kontext.
{
customers: [...],
total_matches: 47,
truncated: true,
next_page_cursor: "abc",
message: "Found 47 matches; showing first 10. Use next_page_cursor to get more."
}
Das Feld message ist menschenlesbare Anleitung. LLMs verwenden es.
Mit sanft behandelten Fehlern.
{
error: "ambiguous_query",
message: "Search term 'john' matched 247 customers. Please be more specific.",
suggestion: "Try including a company name or email domain.",
partial_results: [...] // top 3 by relevance, optional
}
Der Fehler ist strukturiert (maschinenlesbar), enthält aber auch eine Nachricht und einen Vorschlag (LLM-lesbar). Der LLM kann sich anpassen — entweder den Benutzer um Klarstellung bitten oder die Abfrage verfeinern.
In angemessener Größe.
Ein Tool, das 10.000 Datensätze zurückgibt, ist unbrauchbar. Der Kontext des LLM passt nicht hinein; selbst wenn es könnte, würde der LLM es nicht gut nutzen. Paginieren, kürzen oder zusammenfassen Sie immer. Geben Sie genug zurück, damit der LLM eine Entscheidung treffen kann, nicht alles, was existiert.
Muster 4: Fehlersemantik
Tools können fehlschlagen. Wie sie den Fehler dem LLM kommunizieren, bestimmt, ob der LLM sich sanft erholen oder den Fehler verschlimmern kann.
Kategorien von Fehlern.
type ToolError =
| { type: "validation"; message: string; field?: string }
| { type: "auth"; message: string }
| { type: "not_found"; message: string; suggestion?: string }
| { type: "conflict"; message: string; resolution?: string }
| { type: "rate_limit"; message: string; retry_after_seconds: number }
| { type: "service_unavailable"; message: string; retryable: boolean }
| { type: "internal"; message: string; trace_id: string };
Jede Kategorie hat unterschiedliche Semantik. Der LLM sollte anders reagieren:
validation: Eingabe korrigieren und erneut versuchen.not_found: Benutzer informieren oder eine andere Suche durchführen.conflict: Lösung anfordern.rate_limit: Warten und erneut versuchen.service_unavailable: Fallback verwenden oder Benutzer informieren.internal: Aufgeben und dem Benutzer präsentieren.
Dokumentieren Sie diese in Ihrem Server, um den LLM fähiger zu machen.
Fehlerformatierung.
Geben Sie Fehler als strukturierte Daten zurück, mit klaren, handlungsorientierten Nachrichten:
{
error: {
type: "validation",
message: "The email address is not in a valid format.",
field: "email",
suggestion: "Provide a valid email address like 'name@example.com'."
}
}
Vermeiden Sie:
{
error: "Invalid input"
}
Die erste Option ermöglicht dem LLM, sich zu erholen. Die zweite lässt es raten.
Muster 5: Authentifizierung und Autorisierung
Produktionsreife MCP-Server benötigen Authentifizierung. Ohne sie kann jeder erreichbare Client die Tools nutzen – das ist fast immer inakzeptabel.
Authentifizierung: Wer ruft an?
Gängige Muster:
- API-Schlüssel. Einfach, verbreitet und für Service-to-Service geeignet. Stellen Sie einen Schlüssel je Consumer aus und rotieren Sie ihn regelmäßig.
- OAuth. Für mehrere Nutzer-Systeme, bei denen Endnutzer Agenten autorisieren. Komplexer, aber die richtige Lösung für viele Anwendungsfälle.
- mTLS. Für hochsichere Umgebungen. Mutual TLS-Zertifikate für beide Seiten.
Die Umsetzung hängt vom Transport ab. Über HTTP authentifizieren Sie die Anfrage, bevor sie den MCP-Handler erreicht (in Ihrem Express/Hono/Fastify-Middleware), und speichern den Aufrufer in der Anfrage:
// Express-style middleware in front of the MCP HTTP endpoint.
app.use("/mcp", async (req, res, next) => {
const apiKey = req.header("x-api-key");
const caller = await authenticate(apiKey);
if (!caller) return res.status(401).send("Unauthorized");
(req as any).caller = caller;
next();
});
Lesen Sie den Aufrufer anschließend in jedem Tool-Handler aus dem Aufrufkontext (extra) und nicht erneut aus rohen Headern. Über stdio gibt es keine HTTP-Header; die Authentifizierung stammt üblicherweise aus Prozessumgebung oder Konfigurationsdateien.
Autorisierung: Was können sie tun?
Nach der Authentifizierung: Welche Tools kann der Aufrufer nutzen und auf welche Daten?
function authorize(caller: Caller, tool: string, params: any): boolean {
// Caller-level: can this caller use this tool at all?
if (!caller.tools.includes(tool)) return false;
// Data-level: is this caller authorized for this specific data?
if (params.tenant_id && params.tenant_id !== caller.tenant_id) return false;
return true;
}
Lassen Sie das LLM niemals Autorisierungsentscheidungen treffen; es kann manipuliert werden. Die Autorisierung wird serverseitig durchgesetzt, und das LLM erhält ausschließlich Daten, für die der Aufrufer berechtigt ist.
In Multi-Tenant-Systemen ist jeder Tool-Aufruf auf einen Mandanten beschränkt. Der Mandant wird aus der Authentifizierung abgeleitet – niemals aus Parametern, die das LLM bereitstellt.
Muster 6: Idempotenz
Bei Schreibvorgängen ist Idempotenz unverzichtbar. Das LLM kann einen Vorgang wiederholen oder dasselbe Tool in unterschiedlichen Kontexten zweimal aufrufen. Ohne Idempotenz entstehen Duplikate.
Idempotenzschlüssel.
Der Tool akzeptiert einen idempotency_key-Parameter. Der Server prüft: haben wir diesen Schlüssel bereits gesehen? Wenn ja, geben Sie das zwischengespeicherte Ergebnis zurück. Wenn nicht, führen Sie aus und speichern Sie es.
async function createInvoice(params: {
amount: number;
customer_id: string;
idempotency_key: string;
}) {
const cached = await idempotencyStore.get(params.idempotency_key);
if (cached) return cached;
const invoice = await actuallyCreateInvoice(params);
await idempotencyStore.set(params.idempotency_key, invoice, { ttl: 86400 });
return invoice;
}
Für den LLM: Hinweis darauf in der Tool-Beschreibung:
"For each unique invoice you create, generate a UUID and pass it as idempotency_key. If you need to retry the operation, use the same UUID to avoid duplicate creation."
Muster 7: Streaming
Für Tools, die große Ausgaben erzeugen oder Zeit benötigen, ist das Streaming der Ausgabe besser für die Benutzererfahrung. MCP unterstützt Fortschrittsbenachrichtigungen innerhalb eines Tool-Handlers über den pro-Aufruf-extra-Argument:
server.registerTool(
"long_running_task",
{ description: "...", inputSchema: { ... } },
async (input, extra) => {
await extra.sendNotification({
method: "notifications/progress",
params: { progressToken: extra._meta?.progressToken, progress: 0, message: "Starting..." },
});
for (const step of steps) {
await doStep(step);
await extra.sendNotification({
method: "notifications/progress",
params: {
progressToken: extra._meta?.progressToken,
progress: step.index / steps.length,
message: step.name,
},
});
}
return { content: [{ type: "text", text: JSON.stringify({ result: finalResult }) }] };
}
);
Verwenden Sie Streaming für:
- Längere Vorgänge (>5 Sekunden).
- Große Ausgaben (damit der LLM mit der Ausgabe beginnen kann, während sie noch kommt).
- Vorgänge mit Zwischenergebnissen, die angezeigt werden sollten.
Verwenden Sie kein Streaming für schnelle, einfache Vorgänge — es fügt Komplexität hinzu, ohne Nutzen.
Muster 8: Caching
Viele Tool-Aufrufe treffen auf dieselben Daten wiederholt. Caching kann die Leistung erheblich verbessern und die Backend-Last reduzieren.
Lokaler Cache. In-Process-Cache (z. B. LRU) für häufig verwendete Daten.
Verteilter Cache. Redis oder eine vergleichbare Lösung als gemeinsamer Cache über mehrere Serverinstanzen.
Cache-Invalidierung. Wenn sich Daten ändern, entfernen Sie relevante Einträge. (Dies ist der schwierige Teil.)
TTLs. Zwischengespeicherte Einträge verfallen nach einer definierten Zeit. Stimmen Sie die Dauer je Datentyp ab: Kundenprofile können stundenlang, Preise nur minutenlang im Cache bleiben.
Damit Caching hilft, müssen dieselben Aufrufe sich wiederholen. Viele MCP-Server sehen dies — Agenten verweisen häufig auf dieselben Entitäten innerhalb einer Sitzung.
Ein Muster:
async function getCustomerCached(id: string) {
const cached = await cache.get(`customer:${id}`);
if (cached) {
metrics.increment("cache.hit");
return cached;
}
metrics.increment("cache.miss");
const customer = await db.getCustomer(id);
await cache.set(`customer:${id}`, customer, { ttl: 300 });
return customer;
}
Muster 9: Rate Limiting
LLM-Agenten können überraschend aggressiv sein — Schleifen, erneutes Versuchen, Ausbreitung. Ein fehlgeleiteter Agent kann Ihren Backend-Server überlasten.
Rate Limiting pro Aufrufer ist unerlässlich:
const limiter = new RateLimiter({
windowMs: 60_000,
max: 100 // 100 calls/minute per caller
});
server.setRequestHandler(CallToolRequestSchema, async (request, context) => {
if (await limiter.exceeded(context.caller.id)) {
return errorResponse("rate_limit", "Too many requests");
}
// ...
});
Neben globalen Rate Limits sind pro-Tool-Limits wichtig — einige Tools sind teuer und sollten eng begrenzt werden.
Für folgenreiche Vorgänge wie das Erstellen von Datensätzen oder Versenden von Nachrichten gelten strengere Limits oder ausdrückliche Bestätigungsabläufe.
Muster 10: Ressourcen
MCP hat „Ressourcen“ — nur lesbare Datenquellen, die der LLM durchsuchen und beziehen kann. Unterschiedlich von Tools (die aktiv aufgerufen werden).
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "doc://my-server/handbook",
name: "Employee Handbook",
mimeType: "text/markdown",
description: "Company employee handbook"
},
// ...
]
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const content = await loadResource(request.params.uri);
return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: content }] };
});
Ressourcen sind nützlich für:
- Referenzdokumente, die der LLM durchsuchen möchte.
- Konfigurations- oder Kontextdaten.
- Suchtabellen oder Schemata, die der LLM benötigen könnte.
Ressourcen werden gelesen; Tools sind Aktionen. Verwenden Sie das richtige Konzept für jedes.
Muster 11: Observability
Gleiche Muster wie anderswo in der Produktions-KI. Für Ihren MCP-Server instrumentieren Sie:
- Jeder Tool-Aufruf: Zeitstempel, Aufrufer, Tool, Parameter, Ergebnis, Latenz, Status.
- Pro-Tool-Metriken: Aufrufvolumen, p50/p95-Latenz, Fehlerquote.
- Pro-Aufrufer-Metriken: Wer ruft an, wie oft.
- Trace-Kontext: Propagieren Sie Trace-IDs vom Aufrufer bis zu Backend-Aufrufen.
Strukturierte Protokolle:
logger.info("tool_call", {
tool: request.params.name,
caller_id: context.caller.id,
trace_id: context.trace_id,
params: redactPII(request.params.arguments),
duration_ms: duration,
status: "success"
});
Leiten Sie diese Logs an Ihre Observability-Plattform weiter.
Muster 12: Versionierung
Ihr MCP-Server wird sich entwickeln. Tools werden sich ändern. Neue Tools werden hinzugefügt. Alte Tools werden veraltet.
Serverversionierung. Der Server-Konstruktor nimmt eine Version entgegen. Erhöhen Sie diese bei Änderungen, damit Clients sie erkennen können.
Toolversionierung. Ändert sich die Signatur eines Tools inkompatibel, versionieren Sie es, etwa als search_customers_v2. Halten Sie die alte Version während einer Übergangsfrist verfügbar.
Schemaentwicklung. Optionale Felder lassen sich sicher ergänzen. Das Entfernen von Feldern oder Ändern von Typen ist eine Breaking Change.
Deprecation. Kennzeichnen Sie ein veraltetes Tool in seiner Beschreibung: „VERALTET: Verwenden Sie stattdessen search_customers_v2.“
Für produktionsreife MCP-Server, die von mehreren Clients genutzt werden, ist Versionierung unerlässlich. Interne Server können flexibler sein.
Muster 13: Testen
Wie testen Sie einen MCP-Server?
Einheitstests. Die Logik jedes Tools mit gemockten Abhängigkeiten. Standard TypeScript-Testen.
Schema-Tests. Schemata validieren sich wie erwartet. Randfälle (fehlende Felder, falsche Typen) werden korrekt behandelt.
Integrationstests. Starten Sie den Server, senden Sie tatsächliche MCP-Anfragen, überprüfen Sie die Antworten. Der @modelcontextprotocol/sdk enthält Test-Utilities.
End-to-End mit einem echten LLM. Die schwierigste, aber wertvollste. Lassen Sie ein LLM Ihren MCP-Server verwenden, um realistische Aufgaben auszuführen. Überprüfen Sie, ob das LLM die Tools korrekt nutzt. Finden Sie Probleme in den Tool-Beschreibungen.
Ein End-to-End-Test-Setup (Pseudocode; die genaue Client-Verdrahtung hängt davon ab, welchen LLM-Client Sie verwenden — Anthropics TypeScript SDK, OpenAIs oder ein Framework, das MCP unterstützt):
// Start your MCP server as a child process or in-memory transport.
const server = await startTestServer();
// Drive an LLM with the MCP tools attached. The exact API depends on the client.
const result = await runAgent({
mcpServer: server,
systemPrompt: "You are a customer service agent...",
userMessage: "Find the customer Alice and check her open tickets",
});
// Inspect the tool calls captured by the server during the run.
expect(server.callLog.map((c) => c.name)).toEqual([
"search_customers",
"list_tickets",
]);
End-to-End-Tests erfassen Probleme in Tool-Beschreibungen, die Einheitstests nicht erkennen können.
Muster 14: Deployment
Wo lebt Ihr MCP-Server?
Stdio (lokal). Der Server läuft als Prozess und wird vom Client gestartet. Dies eignet sich für Desktop-Anwendungen wie Claude Desktop und Cursor sowie für lokale Tools.
Streamable HTTP (remote). Der Server ist ein Netzwerkdienst. Dies eignet sich für gehostete Services, gemeinsam genutzte Infrastruktur und den Zugriff mehrerer Clients.
Für produktionsreife Server:
- Streamable HTTP ist in der Regel die Wahl.
- Stellen Sie ihn wie jeden Webservice bereit: Container, Load Balancing und Autoscaling.
- TLS erforderlich.
- Health Checks für die Deployment-Plattform.
- Graceful Shutdown für laufende Anfragen.
Muster 15: Sicherheitsaspekte
MCP-Server exponieren Fähigkeiten für LLMs. LLMs können manipuliert werden. Sicherheitsaspekte:
Prompt Injection über Tool-Eingaben. Eine Nutzeranfrage kann Text enthalten, der das LLM zu schädlichen Tool-Aufrufen verleiten soll. Schutzmaßnahmen:
- Klare Tool-Beschreibungen über erwartete Nutzung.
- Autorisierung auf Serverseite (unabhängig von LLM-basierten Parametern).
- Bestätigungen für folgenreiche Aktionen.
Datenexfiltration. Tools, die Daten zurückgeben, können missbraucht werden: Das LLM kann dazu verleitet werden, sensible Daten unzulässig offenzulegen. Schutzmaßnahmen:
- Autorisierungschecks.
- Protokollierung, welche Daten von wem abgerufen wurden.
- Mustererkennung für ungewöhnliche Zugriffsmuster.
Ressourcenverbrauch. Tools, die Backend-Ressourcen verbrauchen, können missbraucht werden. Schutzmaßnahmen:
- Rate Limiting.
- Ressourcenbegrenzungen pro Tool-Aufruf.
- Circuit Breaker, wenn das Backend beeinträchtigt ist.
Injection in Tool-Ausgaben. Die Ausgabe eines Tools kann Text enthalten, der das LLM beim Lesen manipuliert. Schutzmaßnahmen:
- Ausgaben nach Möglichkeit bereinigen.
- Besondere Vorsicht bei Tools, die nutzergenerierte Inhalte zurückgeben.
Dies sind reale Angriffsflächen. Behandeln Sie MCP-Server wie jede andere produktive API und setzen Sie Defense in Depth um.
Ein vollständiges Beispiel: ein kleiner, aber realer MCP-Server
Um alles zusammenzusetzen, ein Server, der ein kleines CRM exponiert:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { db, cache, logger, authenticate } from "./infra.js";
const server = new McpServer({
name: "crm-server",
version: "1.0.0",
});
// === Tool: search_customers ===
server.registerTool(
"search_customers",
{
description: "Search customers by name, email, or company.",
inputSchema: {
query: z.string().describe("Name, email, or company"),
limit: z.number().int().min(1).max(50).default(10),
},
},
async ({ query, limit }, extra) => {
const auth = await authenticate(extra);
const cacheKey = `search:${auth.tenant_id}:${query}:${limit}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;
const customers = await db.searchCustomers({
tenant_id: auth.tenant_id,
query,
limit,
});
const result = {
content: [{
type: "text" as const,
text: JSON.stringify({
customers,
total_matches: customers.length,
truncated: customers.length === limit,
message:
customers.length === limit
? `Showing first ${limit}; there may be more matches.`
: `Found ${customers.length} customer(s).`,
}),
}],
};
await cache.set(cacheKey, result, { ttl: 60 });
logger.info("search_customers", { tenant: auth.tenant_id, query, results: customers.length });
return result;
}
);
// === Tool: get_customer ===
server.registerTool(
"get_customer",
{
description: "Fetch a single customer by id.",
inputSchema: { customer_id: z.string() },
},
async ({ customer_id }, extra) => {
const auth = await authenticate(extra);
const customer = await db.getCustomer(auth.tenant_id, customer_id);
if (!customer) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Customer ${customer_id} not found. Use search_customers to find by name or email.`,
}],
};
}
return { content: [{ type: "text" as const, text: JSON.stringify({ customer }) }] };
}
);
// === Tool: update_customer_email (with idempotency) ===
server.registerTool(
"update_customer_email",
{
description: "Update a customer's email; pass the same idempotency_key on retry.",
inputSchema: {
customer_id: z.string(),
new_email: z.string().email(),
idempotency_key: z
.string()
.describe("UUID for this update; pass the same value on retry to prevent duplicates"),
},
},
async (params, extra) => {
const auth = await authenticate(extra);
// ... idempotency check, validation, update
return { content: [{ type: "text" as const, text: "ok" }] };
}
);
// ... more tools ...
// Wire up a remote transport (Streamable HTTP) on a chosen port via your HTTP server of choice.
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
await server.connect(transport);
Dies ist eine Startstruktur. Fügen Sie Beobachtbarkeit, Rate Limiting, weitere Tools, sorgfältigere Schemata hinzu — aber die Grundlagen sind hier.
Was produktionsreife Server von Demos unterscheidet
MCP ist ein kleines Protokoll; das Erstellen eines produktionsreifen Servers ist echte Ingenieursarbeit. Der Vorteil: Ihr Dienst wird von jedem LLM-Agenten genutzt werden können, mit einer standardisierten Integration, die keine pro-Modell-Kopplung erfordert.
Die Muster, die zählen: fokussiertes Tool-Design, promptbewusste Schemata, strukturierte Fehlersemantik, robuste Authentifizierung, Idempotenz, Beobachtbarkeit, Sicherheit. Das Überspringen irgendeines dieser Muster führt zu einem MCP-Server, der in der Produktion fehlschlägt.
Implementieren Sie sie. Testen Sie gegen echte LLMs. Iterieren Sie an Tool-Beschreibungen. Das Ergebnis ist ein Dienst, den ein LLM so fluide nutzen kann wie ein Mensch — und der mit der rasch wachsenden Welt der KI-Agenten skalierbar ist.



