# Einführung Source: https://picomcp.github.io/picomcp/de/docs/ > Was picomcp ist, was es nicht ist, und die zehn Zeilen, die es erklären. picomcp ist ein TypeScript-Framework für [Model-Context-Protocol](https://modelcontextprotocol.io)-Server. Ein MCP-Server gibt einer KI-Anwendung (Claude, Cursor, VS Code, deinem eigenen Agenten) **Tools**, die sie aufrufen kann, **Ressourcen**, die sie lesen kann, und **Prompts**, die sie dem Nutzer anbieten kann. ```ts import { pico } from "picomcp"; import { z } from "zod"; pico({ name: "weather" }) .tool("forecast", { description: "Get the forecast for a city.", input: z.object({ city: z.string() }), run: ({ city }) => getForecast(city), }) .serve(); ``` Das ist ein vollständiger Server. Starte ihn mit `node server.ts` und verbinde ihn mit einem beliebigen MCP-Client. ## Warum picomcp - **Klein.** Rund 4 kB gzip-komprimiert, keine Abhängigkeiten. Die ganze Bibliothek passt in deinen Kopf und in ein Code-Review. - **Typisiert aus deinem Schema.** Übergib ein Zod-, Valibot- oder ArkType-Schema. Dein Handler bekommt typisierte Argumente, der Client ein JSON Schema, ungültige Aufrufe eine lesbare Fehlermeldung. - **Läuft überall.** Node 22+, Bun, Deno, Cloudflare Workers und jede Runtime mit `fetch`. Keine Node-Module im Kern. - **Zwei Transporte, ein Server.** stdio für Desktop-Clients, Streamable HTTP für entfernte Server. Eine Option schaltet um. - **Testbar ohne Client.** `server.call("forecast", { city: "Lyon" })` führt ein Tool im selben Prozess aus, vollständig typisiert. - **Mit dem offiziellen SDK geprüft.** Die Testsuite steuert picomcp mit `@modelcontextprotocol/sdk` über beide Transporte. ## Was picomcp nicht ist picomcp implementiert die Serverseite von MCP und sonst nichts. Es enthält keinen Client, keine Agenten-Schleife und kein LLM-SDK. Es konzentriert sich auf den zustandslosen Request/Response-Teil des Protokolls, den die meisten Server brauchen. Vom Server ausgehende Funktionen wie Sampling und Elicitation werden noch nicht unterstützt: Die genaue Liste steht unter [Protokollabdeckung](https://picomcp.github.io/picomcp/de/docs/protocol). ## Nächste Schritte - [Schnellstart](https://picomcp.github.io/picomcp/de/docs/quickstart): in fünf Minuten einen Server bauen und verbinden. - [Tools](https://picomcp.github.io/picomcp/de/docs/tools): alles, was ein Tool kann. - [Für KI-Agenten](https://picomcp.github.io/picomcp/de/docs/agents): gib deinem Coding-Agenten die Doku mit einer einzigen URL. --- # Schnellstart Source: https://picomcp.github.io/picomcp/de/docs/quickstart > In fünf Minuten deinen ersten MCP-Server anlegen, testen und verbinden. Du brauchst Node 22.18 oder neuer (es führt TypeScript-Dateien direkt aus), Bun oder Deno. ## Projekt anlegen ```sh npx picomcp init my-server cd my-server npm install ``` Damit bekommst du einen lauffähigen Server: | Datei | Zweck | | --- | --- | | `server.ts` | Deine Serverdefinition: Tools, Ressourcen, Prompts | | `main.ts` | Startet den Server über stdio | | `server.test.ts` | Ein Test, der ein Tool im selben Prozess aufruft | Lieber bei null anfangen? Installiere die beiden Pakete und schreibe `server.ts` selbst: ```sh npm install picomcp zod ``` ## Ein Tool schreiben Öffne `server.ts`: ```ts import { pico } from "picomcp"; import { z } from "zod"; export const server = pico({ name: "my-server", version: "0.1.0" }) .tool("greet", { description: "Greets someone by name.", input: z.object({ name: z.string().describe("Who to greet") }), run: ({ name }) => `Hello, ${name}!`, }); ``` - `description` liest das Modell, um zu entscheiden, wann es das Tool aufruft. Schreib sie für das Modell. - `input` ist ein beliebiges [Standard Schema](https://picomcp.github.io/picomcp/de/docs/schemas). In `run` ist `name` als `string` typisiert. - Hier gibt `run` einen String zurück. Es kann auch Objekte oder Inhaltsblöcke zurückgeben oder einen Fehler werfen: siehe [Tools](https://picomcp.github.io/picomcp/de/docs/tools). ## Testen ```ts import assert from "node:assert/strict"; import { test } from "node:test"; import { server } from "./server.ts"; test("greet says hello", async () => { const result = await server.call("greet", { name: "Ada" }); assert.deepEqual(result.content, [{ type: "text", text: "Hello, Ada!" }]); }); ``` ```sh npm test ``` `server.call` wird vom Typsystem geprüft: Ein Tippfehler im Tool-Namen oder ein fehlendes Argument lässt sich nicht kompilieren. ## Verbinden Für Claude Code: ```sh claude mcp add my-server -- node /absolute/path/to/my-server/main.ts ``` Bitte Claude dann, jemanden zu begrüßen. Für Claude Desktop, Cursor, VS Code und andere siehe [Client verbinden](https://picomcp.github.io/picomcp/de/docs/clients). ## Untersuchen Mit dem MCP Inspector rufst du Tools von Hand auf und siehst die rohen Nachrichten: ```sh npx @modelcontextprotocol/inspector node main.ts ``` --- # Client verbinden Source: https://picomcp.github.io/picomcp/de/docs/clients > Konfiguration für Claude Code, Claude Desktop, Cursor, VS Code und den MCP Inspector. Jeder MCP-Client startet einen lokalen Server auf dieselbe Weise: Er führt einen Befehl aus und spricht über stdio mit ihm. Entfernte Server erreicht man per URL über Streamable HTTP. Verwende in allen Konfigurationen unten absolute Pfade. ## Claude Code ```sh # local, over stdio claude mcp add weather -- node /absolute/path/main.ts # remote, over HTTP claude mcp add --transport http weather https://example.com/mcp ``` Mit `--scope project` wird der Server in `.mcp.json` geschrieben und mit deinem Team geteilt. ## Claude Desktop Bearbeite `claude_desktop_config.json` (Einstellungen → Entwickler → Konfiguration bearbeiten) und starte die App neu: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] } } } ``` ## Cursor Lege `.cursor/mcp.json` in deinem Projekt an, oder `~/.cursor/mcp.json` für alle Projekte: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "url": "https://example.com/mcp" } } } ``` ## VS Code Lege `.vscode/mcp.json` an: ```json { "servers": { "weather": { "type": "stdio", "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "type": "http", "url": "https://example.com/mcp" } } } ``` ## MCP Inspector ```sh npx @modelcontextprotocol/inspector node /absolute/path/main.ts ``` Um einen HTTP-Server zu untersuchen, starte ihn mit `serve({ http: 3000 })`, öffne den Inspector, wähle **Streamable HTTP** und gib `http://127.0.0.1:3000/mcp` ein. Der Inspector läuft auf localhost, was picomcp standardmäßig erlaubt. ## Bun und Deno Ersetze `node` durch deine Runtime: ```json { "command": "bun", "args": ["/absolute/path/main.ts"] } ``` ```json { "command": "deno", "args": ["run", "--allow-net", "/absolute/path/main.ts"] } ``` ## Fehlerbehebung **Der Client zeigt keine Tools.** Führe den Befehl aus der Konfiguration selbst aus. Wenn er sich beendet oder einen Fehler ausgibt, sieht der Client dasselbe. **„Unexpected token“ oder JSON-Fehler im Client-Log.** Etwas hat auf stdout geschrieben. picomcp leitet `console.log` beim Betrieb über stdio nach stderr um, aber andere Bibliotheken schreiben eventuell direkt in `process.stdout`. **Node kann keine `.ts`-Dateien ausführen.** Aktualisiere auf Node 22.18 oder neuer, oder starte den Server mit Bun. --- # Tools Source: https://picomcp.github.io/picomcp/de/docs/tools > Tools definieren, Eingaben validieren, Text, Objekte oder Medien zurückgeben, Fehler melden und Abbrüche behandeln. Ein Tool ist eine Funktion, die das Modell aufrufen kann. Registriere eines mit `server.tool(name, config)`: ```ts server.tool("search_issues", { title: "Search issues", description: "Search open GitHub issues in a repository. Use it before filing a new issue.", input: z.object({ repo: z.string().describe("owner/name"), query: z.string(), limit: z.number().int().max(50).default(10), }), annotations: { readOnlyHint: true, openWorldHint: true }, run: async ({ repo, query, limit }, { signal }) => { const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:${repo}`)}&per_page=${limit}`, { signal }); const { items } = await res.json(); return items.map((i: { title: string; html_url: string }) => `${i.title} — ${i.html_url}`).join("\n"); }, }); ``` ## Konfiguration | Option | Typ | Beschreibung | | --- | --- | --- | | `description` | `string` | Was das Tool tut und wann man es nutzt. Das Modell liest diesen Text. | | `title` | `string` | Anzeigename für Clients. | | `input` | Standard Schema | Schema der Argumente. Lass es bei Tools ohne Argumente weg. | | `output` | Standard Schema | Schema des strukturierten Ergebnisses. Siehe [Strukturierte Ausgabe](#strukturierte-ausgabe). | | `annotations` | `ToolAnnotations` | Hinweise für Clients: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. | | `run` | `(input, ctx) => value` | Der Handler. Er darf asynchron sein. | Tool-Namen sind 1 bis 128 Zeichen lang und bestehen nur aus Buchstaben, Ziffern, `_`, `.` und `-`. Ein ungültiger oder doppelter Name wirft sofort einen Fehler, sodass Fehler beim Serverstart auffallen und nicht erst, wenn ein Modell das Tool aufruft. ## Eingabevalidierung picomcp validiert die Argumente mit deinem Schema, bevor `run` aufgerufen wird. Standardwerte und Transformationen greifen, daher ist `limit` im Beispiel in `run` immer eine Zahl. Schlägt die Validierung fehl, wird das Tool nicht aufgerufen. Der Client bekommt einen Tool-Fehler, der das fehlerhafte Feld nennt, damit das Modell seinen Aufruf korrigieren und erneut versuchen kann: ```text Invalid arguments for tool "search_issues": limit: Too big: expected number to be <=50 ``` ## Rückgabewerte Gib zurück, was naheliegt. picomcp macht daraus ein MCP-Ergebnis: | `run` gibt zurück | Der Client erhält | | --- | --- | | `string` | Einen Textblock | | ein Objekt, ein Array oder eine Zahl | Sein JSON als Text. Objekte gehen zusätzlich als `structuredContent` raus. | | einen Inhaltsblock oder ein Array davon | Diese Blöcke unverändert | | `{ content: [...] }` | Das Ergebnis unverändert, für volle Kontrolle | | `undefined` | Ein leeres Ergebnis | Inhaltsblöcke baust du mit den Helfern: ```ts import { audio, image, link, text } from "picomcp"; run: async () => [ text("Here is the chart:"), image(await renderPng(), "image/png"), // Uint8Array or base64 string link("file:///reports/q3.csv", "q3.csv", { mimeType: "text/csv" }), ]; ``` ## Strukturierte Ausgabe Deklariere ein `output`-Schema, wenn Clients oder Code das Ergebnis verarbeiten, nicht nur das Modell: ```ts server.tool("get_price", { input: z.object({ sku: z.string() }), output: z.object({ sku: z.string(), cents: z.number().int(), currency: z.literal("EUR") }), run: async ({ sku }) => ({ sku, cents: await priceOf(sku), currency: "EUR" }), }); ``` picomcp dann: 1. kündigt das Schema in `tools/list` als `outputSchema` an, 2. prüft den Rückgabewert von `run` im Typsystem dagegen, 3. validiert den Wert zur Laufzeit und sendet ihn als `structuredContent`, mit einer JSON-Textkopie für ältere Clients. Passt der Wert nicht, bekommt der Client einen Tool-Fehler statt fehlerhafter Daten. ## Fehler Wirf in `run` einen Fehler. Die Fehlermeldung wird zu einem Tool-Fehler (`isError: true`), den das Modell lesen und auf den es reagieren kann: ```ts run: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new Error(`No user with id ${id}. Call list_users to see valid ids.`); return user; }; ``` Schreib Fehlermeldungen für das Modell: Sag, was schiefging und was als Nächstes zu tun ist. Tool-Fehler gehören zu einem normalen Gespräch. Protokollfehler nicht: Verwende `McpError` nur, wenn die Anfrage selbst falsch ist, etwa wenn dem Nutzer der Zugriff fehlt: ```ts import { McpError } from "picomcp"; throw new McpError(-32001, "Not authorised for this workspace"); ``` ## Abbruch Das zweite Argument von `run` enthält ein `AbortSignal`. Es wird ausgelöst, wenn der Client `notifications/cancelled` sendet, meist weil der Nutzer auf Stopp gedrückt hat. Gib es an `fetch` und andere APIs weiter, die es annehmen: ```ts run: async ({ url }, { signal }) => { const res = await fetch(url, { signal }); return res.text(); }; ``` Sobald eine Anfrage abgebrochen ist, sendet picomcp dafür keine Antwort, wie es das Protokoll verlangt. ## Metadaten `ctx.meta` enthält das `_meta`-Objekt, das der Client mit der Anfrage gesendet hat, falls vorhanden. Clients nutzen es für Tracing und Fortschritts-Tokens. ## Gute Tools schreiben - **Eine Aufgabe pro Tool.** `create_invoice` und `send_invoice` sind besser als `invoice(action)`. - **Beschreibe die Argumente.** `.describe()` an jedem Feld landet im JSON Schema, das das Modell sieht. - **Gib weniger zurück.** Modelle bezahlen jedes Token. Gib die wichtigen Felder zurück, nicht die ganze API-Antwort. - **Annotiere ehrlich.** Mit `readOnlyHint: true` können Clients Bestätigungsdialoge überspringen. Setz es nie bei einem Tool, das schreibt. --- # Ressourcen Source: https://picomcp.github.io/picomcp/de/docs/resources > Dateien, Datensätze und Live-Daten bereitstellen, die der Client lesen kann, mit typisierten URI-Templates. Ressourcen sind Daten, die der Client lesen und einem Gespräch hinzufügen kann: Dateien, Datenbankzeilen, API-Antworten. Tools ruft das Modell auf; Ressourcen wählt meist der Nutzer oder die Anwendung aus. ```ts server.resource("config://app", { description: "The app configuration", read: () => loadConfig(), }); ``` ## URI-Templates Setz `{variables}` in die URI, um eine ganze Familie von Ressourcen bereitzustellen. Die Variablen werden aus dem String selbst typisiert: ```ts server.resource("github://{owner}/{repo}/readme", { mimeType: "text/markdown", read: async ({ owner, repo }) => { // ^? { owner: string; repo: string } const res = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/README.md`); return res.text(); }, }); ``` | Syntax | Passt auf | Beispiel | | --- | --- | --- | | `{name}` | Ein Pfadsegment, ohne `/`, `?` oder `#` | `users://{id}` passt auf `users://42` | | `{+name}` | Alles, auch `/` | `files://{+path}` passt auf `files://src/index.ts` | Die Werte werden URL-dekodiert, bevor sie `read` erreichen. Statische Ressourcen erscheinen in `resources/list`, Templates in `resources/templates/list`. ## Rückgabewerte | `read` gibt zurück | Gesendeter Inhalt | | --- | --- | | `string` | Text, mit `mimeType` oder `text/plain` | | `Uint8Array` | Base64-Blob, mit `mimeType` oder `application/octet-stream` | | ein Array von `{ uri, text }` oder `{ uri, blob }` | Diese Inhalte unverändert | | jeder andere Wert | Sein JSON, als `application/json` | `read` bekommt den Kontext als zweites Argument, ergänzt um die angefragte `uri`: ```ts read: (params, { uri, signal }) => fetch(toHttp(uri), { signal }).then((r) => r.text()); ``` ## Optionen | Option | Beschreibung | | --- | --- | | `name` | Kennung, die Nutzern angezeigt wird. Standard ist die URI. | | `title` | Anzeigename. | | `description` | Was die Ressource enthält. | | `mimeType` | Inhaltstyp dessen, was `read` zurückgibt. | | `annotations` | Hinweise zu `audience`, `priority` und `lastModified`. | ## Nicht gefunden Passt keine Ressource auf eine URI, antwortet picomcp mit dem Protokollfehler `-32002 Resource not found`. Um eine URI abzulehnen, die zwar auf ein Template passt, aber auf nichts zeigt, wirf eine `McpError` mit demselben Code: ```ts import { McpError } from "picomcp"; read: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new McpError(-32002, `Resource not found: users://${id}`); return user; }; ``` ## Aus Tools verlinken Ein Tool kann das Modell auf eine Ressource verweisen, statt sie einzubetten. So bleiben große Daten aus dem Kontext, bis sie gebraucht werden: ```ts import { link } from "picomcp"; run: async () => link("reports://2026-q3", "Q3 report", { mimeType: "application/pdf" }); ``` --- # Prompts Source: https://picomcp.github.io/picomcp/de/docs/prompts > Wiederverwendbare Prompt-Vorlagen anbieten, die Nutzer im Client auswählen, mit validierten Argumenten. Prompts sind Vorlagen, die der Nutzer im Client auswählt, oft als Slash-Befehle. Sie machen aus ein paar Argumenten eine oder mehrere Nachrichten. ```ts server.prompt("review", { title: "Code review", description: "Review a diff for bugs and style issues.", args: z.object({ diff: z.string().describe("The diff to review"), focus: z.string().optional().describe("What to pay attention to"), }), run: ({ diff, focus }) => `Review this diff${focus ? `, focusing on ${focus}` : ""}:\n\n${diff}`, }); ``` Der Client zeigt `diff` als Pflichtfeld und `focus` als optional an, jeweils mit Beschreibung, alles aus dem Schema abgeleitet. ## Argumente sind Strings MCP überträgt Prompt-Argumente als Strings. Verwende String-Schemas oder konvertiere: ```ts args: z.object({ count: z.coerce.number().int().min(1) }); ``` Ungültige Argumente werden mit einem `Invalid params`-Fehler abgewiesen, bevor `run` aufgerufen wird. ## Rückgabewerte | `run` gibt zurück | Gesendete Nachrichten | | --- | --- | | `string` | Eine Nutzernachricht | | `PromptMessage[]` | Diese Nachrichten | | `{ description, messages }` | Das vollständige Ergebnis | Nimm ein Array für Vorlagen mit mehreren Runden oder um Medien einzubinden: ```ts import { image, text } from "picomcp"; run: async ({ id }) => [ { role: "user", content: text("What is wrong with this screenshot?") }, { role: "user", content: image(await screenshot(id), "image/png") }, ]; ``` --- # Schemas Source: https://picomcp.github.io/picomcp/de/docs/schemas > Zod, Valibot, ArkType oder jede Standard-Schema-Bibliothek verwenden, und wie das JSON Schema entsteht. picomcp bringt keine Schema-Bibliothek mit. Es akzeptiert jede Bibliothek, die [Standard Schema](https://standardschema.dev) implementiert, und erzeugt das JSON Schema, das MCP-Clients brauchen, über [Standard JSON Schema](https://standardschema.dev/json-schema). Du behältst die Bibliothek, die du schon nutzt, und picomcp bleibt klein. ## Zod 4 Funktioniert direkt: ```ts import { z } from "zod"; server.tool("forecast", { input: z.object({ city: z.string().describe("City name") }), run: ({ city }) => getForecast(city), }); ``` ## ArkType Funktioniert direkt: ```ts import { type } from "arktype"; server.tool("forecast", { input: type({ city: "string" }), run: ({ city }) => getForecast(city), }); ``` ## Valibot Valibot hält die JSON-Schema-Unterstützung in einem eigenen Paket. Umschließe das Schema mit `toStandardJsonSchema`: ```ts import * as v from "valibot"; import { toStandardJsonSchema } from "@valibot/to-json-schema"; server.tool("forecast", { input: toStandardJsonSchema(v.object({ city: v.string() })), run: ({ city }) => getForecast(city), }); ``` ## Jede andere Bibliothek Validiert eine Bibliothek über Standard Schema, kann aber kein JSON Schema erzeugen (zum Beispiel Zod 3), hängst du selbst eines mit `withJsonSchema` an: ```ts import { withJsonSchema } from "picomcp"; server.tool("forecast", { input: withJsonSchema(schema, { type: "object", properties: { city: { type: "string" } }, required: ["city"], }), run: ({ city }) => getForecast(city), }); ``` Ohne das wirft die Registrierung des Tools einen Fehler, der genau das sagt. Das Problem erreicht also nie einen Client. ## Wie die Typen fließen - `run` bekommt den **Ausgabetyp** des Schemas, nach Standardwerten und Transformationen. - `server.call()` akzeptiert den **Eingabetyp** des Schemas, daher sind Felder mit Standardwert in Tests optional. - Mit einem `output`-Schema muss `run` dessen Eingabetyp zurückgeben. ## Tipps für Schemas, die Modelle verstehen - Beschreibe jedes Feld. Die Beschreibungen sind die Dokumentation, die das Modell liest. - Bevorzuge Enums gegenüber Freitext, wenn die möglichen Werte bekannt sind. - Nutze Standardwerte für optionales Verhalten, damit das Modell Argumente weglassen kann. - Halte Objekte flach. Tief verschachtelte Argumente fallen Modellen schwerer. --- # Transporte Source: https://picomcp.github.io/picomcp/de/docs/transports > stdio für lokale Clients, Streamable HTTP für entfernte, und der fetch-Handler, der beides antreibt. Ein Transport überträgt JSON-RPC-Nachrichten zwischen dem Client und deinem Server. picomcp unterstützt die beiden, die MCP definiert. Deine Tools ändern sich dabei nicht. ## stdio ```ts await server.serve(); ``` Der Client startet deinen Server als Kindprozess und tauscht zeilengetrenntes JSON über stdin und stdout aus. So arbeiten Desktop-Clients und Coding-Agenten mit lokalen Servern. stdout gehört dem Protokoll. Beim Betrieb über stdio leitet picomcp `console.log`, `console.info` und `console.debug` nach stderr um, damit eine verirrte Logzeile die Verbindung nicht kaputt macht. Clients zeigen stderr meist in ihren Logs an. ## Streamable HTTP ```ts const running = await server.serve({ http: 3000 }); console.error(`Listening on ${running.url}`); // http://127.0.0.1:3000/mcp ``` `serve({ http })` startet einen Server auf Bun, Deno oder Node, je nachdem, was gerade läuft. Optionen: | Option | Standard | Beschreibung | | --- | --- | --- | | `port` | `3000` | Port, auf dem gelauscht wird. `0` wählt einen freien. | | `hostname` | `"127.0.0.1"` | Mit `"0.0.0.0"` werden Verbindungen von anderen Rechnern angenommen. | | `path` | `"/mcp"` | Pfad des Endpunkts. Andere Pfade antworten mit 404. | Es gibt `{ url, close() }` zurück. ## Der fetch-Handler `server.fetch` ist eine webstandardkonforme Funktion `(Request) => Promise`. Gib sie jeder Runtime oder jedem Framework, das `fetch` spricht: ```ts Bun.serve({ port: 3000, fetch: server.fetch }); ``` ```ts Deno.serve({ port: 3000 }, server.fetch); ``` ```ts export default { fetch: server.fetch }; // Cloudflare Workers ``` `server.fetch` schaut nicht auf den Pfad. Leite die Anfragen selbst dorthin. Weitere Plattformen unter [Deployment](https://picomcp.github.io/picomcp/de/docs/deploy). ## Wie picomcp HTTP spricht picomcp nutzt Streamable HTTP im zustandslosen JSON-Modus: - `POST` mit einer JSON-RPC-Nachricht oder einem Batch → `200` mit JSON-Antwort, oder `202` bei Benachrichtigungen. - `GET` und `DELETE` → `405`. Das signalisiert Clients, dass kein Stream vom Server zum Client angeboten wird. - `OPTIONS` → CORS-Preflight für erlaubte Origins. - Ein nicht unterstützter `MCP-Protocol-Version`-Header → `400`. Es gibt keine Sessions zu speichern, also kann jede Instanz jede Anfrage beantworten. Damit passt picomcp ohne Zusatzaufwand zu Serverless-Plattformen, Load Balancern und Autoscaling. ## Origins und DNS-Rebinding Browser senden einen `Origin`-Header. Eine bösartige Seite könnte darüber einen Server auf deinem Rechner erreichen, deshalb prüft picomcp ihn: - Anfragen ohne `Origin` (CLIs, Server, Desktop-Apps) sind immer erlaubt. - Standardmäßig sind nur die Origins `localhost`, `127.0.0.1` und `[::1]` erlaubt, auf jedem Port. - Andere Origins bekommen `403`. Weitere erlaubst du mit der Option `origins`: ```ts pico({ name: "notes", origins: ["https://claude.ai", "https://app.example.com"] }); pico({ name: "public", origins: "*" }); // any origin: only for public, unauthenticated servers ``` ## Eigene Transporte `server.handle(message)` verarbeitet eine JSON-RPC-Nachricht oder einen Batch und gibt die Antwort zurück. Bei Benachrichtigungen gibt es `undefined` zurück. Damit bindest du picomcp an WebSockets, Message Queues oder alles andere an: ```ts socket.on("message", async (data) => { const response = await server.handle(JSON.parse(data)); if (response) socket.send(JSON.stringify(response)); }); ``` --- # Testen Source: https://picomcp.github.io/picomcp/de/docs/testing > Tools im selben Prozess mit vollen Typen testen, dann das Protokoll Ende zu Ende. ## Tools im selben Prozess aufrufen `server.call(name, input)` führt ein Tool genau so aus, wie es ein Client täte, ohne Transport und ohne Unterprozess. Validierung, Standardwerte, Ausgabeprüfung und Fehlerbehandlung greifen alle. ```ts import { describe, expect, it } from "vitest"; import { server } from "./server.ts"; describe("forecast", () => { it("returns the forecast", async () => { const result = await server.call("forecast", { city: "Lyon" }); expect(result.isError).toBeUndefined(); expect(result.structuredContent).toMatchObject({ city: "Lyon" }); }); it("explains invalid input to the model", async () => { const result = await server.call("forecast", { city: "Lyon", days: 30 }); expect(result.isError).toBe(true); }); }); ``` Das funktioniert mit jedem Test-Runner: Vitest, `node:test`, `bun test`, `deno test`. ## Typgeprüfte Aufrufe Werden Tools verkettet registriert, kennt `call` ihre Namen und Eingaben: ```ts const server = pico({ name: "weather" }).tool("forecast", { input: z.object({ city: z.string(), days: z.number().default(3) }), run: ({ city, days }) => `${city}: ${days} days`, }); server.call("forecast", { city: "Lyon" }); // ✓ days is optional: it has a default server.call("forecast", {}); // ✗ city is missing server.call("forcast", { city: "Lyon" }); // ✗ no tool named forcast ``` Registrierst du Tools in getrennten Anweisungen, kann der Typ von `server` ihnen nicht folgen, und `call` akzeptiert jeden Namen. Verkette die Aufrufe oder weise neu zu: `const app = server.tool(...)`. ## Das Protokoll testen `server.handle` nimmt rohes JSON-RPC entgegen. Das ist praktisch, um Listen und Schemas zu prüfen: ```ts const res = await server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }); expect(res).toMatchSnapshot(); ``` Ein Snapshot von `tools/list` zeigt dir mit wenig Aufwand, wenn eine Schemaänderung verändert, was Modelle sehen. ## Mit einem echten Client testen Für Ende-zu-Ende-Tests steuerst du den Server mit dem offiziellen SDK-Client über HTTP: ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const running = await server.serve({ http: { port: 0 } }); const client = new Client({ name: "test", version: "1.0.0" }); await client.connect(new StreamableHTTPClientTransport(new URL(running.url!))); const { tools } = await client.listTools(); await client.close(); await running.close(); ``` Die Testsuite von picomcp macht genau das, über stdio und über HTTP. --- # Deployment Source: https://picomcp.github.io/picomcp/de/docs/deploy > Einen entfernten MCP-Server auf Cloudflare Workers, Deno Deploy, Bun, Node, Hono oder Next.js betreiben, mit Authentifizierung. Einen entfernten Server erreicht man per URL über Streamable HTTP. Weil picomcp zustandslos ist, läuft es ohne zusätzliche Einrichtung auf Serverless-Plattformen. ## Cloudflare Workers ```ts import { server } from "./server.ts"; export default { fetch(request: Request) { if (new URL(request.url).pathname !== "/mcp") return new Response("Not found", { status: 404 }); return server.fetch(request); }, }; ``` ```sh npx wrangler deploy src/worker.ts ``` ## Deno Deploy ```ts import { server } from "./server.ts"; Deno.serve((request) => server.fetch(request)); ``` ## Bun ```ts Bun.serve({ port: Number(process.env.PORT ?? 3000), routes: { "/mcp": server.fetch }, }); ``` ## Node ```ts await server.serve({ http: { port: Number(process.env.PORT ?? 3000), hostname: "0.0.0.0" } }); ``` ## Hono ```ts import { Hono } from "hono"; const app = new Hono(); app.all("/mcp", (c) => server.fetch(c.req.raw)); export default app; ``` ## Next.js ```ts // app/mcp/route.ts import { server } from "@/lib/mcp"; export const POST = server.fetch; export const GET = server.fetch; export const OPTIONS = server.fetch; ``` ## Authentifizierung picomcp kümmert sich nicht selbst um Authentifizierung: Umschließe `server.fetch` und prüfe die Anfrage, bevor sie deine Tools erreicht. ```ts export default { async fetch(request: Request, env: { TOKEN: string }) { if (request.headers.get("authorization") !== `Bearer ${env.TOKEN}`) { return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } }); } return server.fetch(request); }, }; ``` Clients senden den Header aus ihrer Konfiguration, zum Beispiel: ```sh claude mcp add --transport http notes https://notes.example.com/mcp --header "Authorization: Bearer $TOKEN" ``` Für OAuth setzt du die Middleware deines Anbieters vor `server.fetch` und folgst der [MCP-Autorisierungsspezifikation](https://modelcontextprotocol.io/specification/latest/basic/authorization). ## Checkliste - Über HTTPS ausliefern. - Authentifizierung verlangen, außer der Server ist öffentlich und nur lesend. - `origins` auf die Browser-Origins setzen, die du erwartest, falls es welche gibt. - Secrets in Umgebungsvariablen halten, nie in Tool-Beschreibungen oder -Ergebnissen. - Tool-Argumente als nicht vertrauenswürdige Eingaben behandeln: Das Modell kann von Inhalten beeinflusst werden, die es liest. --- # API-Referenz Source: https://picomcp.github.io/picomcp/de/docs/api > Alle Exporte von picomcp, mit Signaturen. ```ts import { pico, McpError, PROTOCOL_VERSIONS, text, image, audio, link, withJsonSchema } from "picomcp"; import type { Server, ToolConfig, ResourceConfig, PromptConfig, Context, CallToolResult } from "picomcp"; ``` ## pico(info) Erstellt einen Server. ```ts function pico(info: ServerInfo): Server; interface ServerInfo { name: string; // machine-friendly name, e.g. "weather" version?: string; // defaults to "0.0.0" title?: string; // display name instructions?: string; // how the model should use the server as a whole origins?: string[] | "*"; // allowed browser origins for HTTP; defaults to localhost } ``` ## server.tool(name, config) Registriert ein Tool und gibt den Server zurück. Siehe [Tools](https://picomcp.github.io/picomcp/de/docs/tools). ```ts interface ToolConfig { title?: string; description?: string; input?: I; // Standard Schema output?: O; // Standard Schema; enables structuredContent annotations?: ToolAnnotations; run(input: Infer, ctx: Context): Return | Promise; } interface ToolAnnotations { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; } ``` Wirft einen `TypeError` bei einem ungültigen oder doppelten Namen oder bei einem Schema ohne JSON-Schema-Unterstützung. ## server.resource(uri, config) Registriert eine Ressource, oder ein Ressourcen-Template, wenn `uri` `{variables}` enthält. Gibt den Server zurück. Siehe [Ressourcen](https://picomcp.github.io/picomcp/de/docs/resources). ```ts interface ResourceConfig { name?: string; title?: string; description?: string; mimeType?: string; annotations?: Annotations; read(params: UriParams, ctx: Context & { uri: string }): ResourceReturn | Promise; } ``` ## server.prompt(name, config) Registriert einen Prompt und gibt den Server zurück. Siehe [Prompts](https://picomcp.github.io/picomcp/de/docs/prompts). ```ts interface PromptConfig { title?: string; description?: string; args?: A; // Standard Schema of an object with string fields run(args: Infer, ctx: Context): string | PromptMessage[] | { description?: string; messages: PromptMessage[] }; } ``` ## server.call(name, input?) Führt ein Tool im selben Prozess aus und gibt sein `CallToolResult` zurück. Typgeprüft, wenn die Tools verkettet registriert wurden. Wird bei einem unbekannten Tool-Namen abgelehnt. Siehe [Testen](https://picomcp.github.io/picomcp/de/docs/testing). ```ts call(name: string, input?: unknown): Promise; interface CallToolResult { content: ContentBlock[]; structuredContent?: Record; isError?: boolean; } ``` ## server.serve(options?) Startet den Server über stdio, oder über Streamable HTTP, wenn `http` gesetzt ist. Siehe [Transporte](https://picomcp.github.io/picomcp/de/docs/transports). ```ts serve(options?: { http?: number | { port?: number; hostname?: string; path?: string } }): Promise<{ url?: string; close(): Promise }>; ``` ## server.fetch(request) Webstandardkonformer Streamable-HTTP-Handler. An den Server gebunden, lässt sich also als Wert weitergeben. ```ts fetch(request: Request): Promise; ``` ## server.handle(message) Verarbeitet eine JSON-RPC-Nachricht oder einen Batch. Gibt die Antwort zurück, oder `undefined` bei Benachrichtigungen. ```ts handle(message: unknown): Promise; ``` ## Context Das zweite Argument jedes Handlers. ```ts interface Context { signal: AbortSignal; // aborted when the client cancels meta: Record | undefined; // the request's _meta } ``` ## Inhalts-Helfer ```ts text(text: string, annotations?: Annotations): ContentBlock; image(data: string | Uint8Array, mimeType: string): ContentBlock; audio(data: string | Uint8Array, mimeType: string): ContentBlock; link(uri: string, name: string, extra?: { title?: string; description?: string; mimeType?: string }): ContentBlock; ``` `data` ist Base64 oder rohe Bytes. Bytes werden auf jeder Runtime für dich kodiert. ## withJsonSchema(schema, jsonSchema, output?) Hängt ein JSON Schema an ein Standard Schema an, das selbst keines erzeugen kann. Siehe [Schemas](https://picomcp.github.io/picomcp/de/docs/schemas). ```ts withJsonSchema(schema: S, jsonSchema: JsonSchema, output?: JsonSchema): StandardSchemaV1, Infer>; ``` ## McpError Wirf sie in einem Handler, um mit einem JSON-RPC-Fehler statt eines Ergebnisses zu antworten. ```ts new McpError(code: number, message: string, data?: unknown); ``` | Code | Bedeutung | | --- | --- | | `-32602` | Ungültige Parameter | | `-32002` | Ressource nicht gefunden | | `-32000` bis `-32099` | Frei für eigene Serverfehler | ## PROTOCOL_VERSIONS Die MCP-Protokollversionen, die picomcp spricht, neueste zuerst: `["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]`. ## Typ-Helfer | Typ | Beschreibung | | --- | --- | | `Infer` | Ausgabetyp eines Schemas | | `InferInput` | Eingabetyp eines Schemas | | `UriParams<"a://{x}/{+y}">` | `{ x: string; y: string }` | | `StandardSchemaV1` | Das Standard-Schema-Interface, eingebettet | --- # Protokollabdeckung Source: https://picomcp.github.io/picomcp/de/docs/protocol > Welche Teile der MCP-Spezifikation picomcp genau implementiert und welche noch nicht. picomcp zielt auf MCP `2025-11-25` und handelt bei Bedarf `2025-06-18`, `2025-03-26` und `2024-11-05` aus. Jede als unterstützt markierte Funktion ist durch Tests abgedeckt, darunter Ende-zu-Ende-Tests mit dem Client des offiziellen TypeScript-SDK. ## Unterstützt | Bereich | Funktion | | --- | --- | | Lebenszyklus | `initialize` mit Versionsaushandlung, `notifications/initialized`, `ping` | | Tools | `tools/list`, `tools/call`, Eingabevalidierung, `outputSchema` und `structuredContent`, Annotationen, Titel | | Inhalte | Text, Bild, Audio, Ressourcen-Links, eingebettete Ressourcen | | Ressourcen | `resources/list`, `resources/templates/list`, `resources/read`, Text- und Binärinhalte | | Prompts | `prompts/list`, `prompts/get`, aus Schemas abgeleitete Argumente | | Hilfsfunktionen | `notifications/cancelled` mit `AbortSignal` | | JSON-RPC | Batches, Fehlercodes, Antworten des Clients werden ignoriert | | stdio | Zeilengetrenntes JSON, stdout vor verirrten Logs geschützt | | Streamable HTTP | Zustandslose JSON-Antworten, `202` für Benachrichtigungen, `405` für `GET`, CORS, Prüfung von `Origin` und `MCP-Protocol-Version` | ## Noch nicht unterstützt | Funktion | Warum | | --- | --- | | Sampling, Elicitation, Roots | Dafür muss der Server Anfragen an den Client senden. Das zustandslose Design von picomcp hält dafür noch keinen Kanal offen. | | Log-, Fortschritts- und `list_changed`-Benachrichtigungen | Gleicher Grund: Nachrichten vom Server an den Client. | | Ressourcen-Abonnements | Gleicher Grund. | | Vervollständigungen | Geplant. | | Paginierungs-Cursor | Listen werden vollständig zurückgegeben. Das passt für Server mit bis zu einigen Hundert Einträgen. | | Sessions (`Mcp-Session-Id`) und SSE-Streams | Im zustandslosen Modus nicht nötig. | | Tasks | In der Spezifikation noch experimentell. | Du brauchst eine davon? [Eröffne ein Issue](https://github.com/picomcp/picomcp/issues) und beschreibe deinen Anwendungsfall. Funktionen kommen hinzu, wenn sie sich hinzufügen lassen, ohne picomcp groß zu machen. --- # Für KI-Agenten Source: https://picomcp.github.io/picomcp/de/docs/agents > Maschinenlesbare Doku, llms.txt, Markdown-Seiten und Regeln für deinen Coding-Agenten. Coding-Agenten schreiben viele MCP-Server. Diese Doku ist so gebaut, dass sie sie genauso gut lesen können wie du. ## Maschinenlesbare Doku | URL | Inhalt | | --- | --- | | [`/llms.txt`](https://picomcp.github.io/picomcp/llms.txt) | Index aller Seiten mit einer einzeiligen Zusammenfassung, im [llms.txt](https://llmstxt.org)-Format | | [`/llms-full.txt`](https://picomcp.github.io/picomcp/llms-full.txt) | Die gesamte Dokumentation in einer Markdown-Datei | | `/docs/.md` | Jede Seite als Markdown. Häng `.md` an ihre URL, zum Beispiel [`/docs/tools.md`](https://picomcp.github.io/picomcp/de/docs/tools.md) | Jede Seite hat außerdem einen Button **Als Markdown kopieren**, um sie in einen Chat einzufügen. Diese Dateien sind auf Englisch, der Referenzsprache, der Modelle am besten folgen. Jede Übersetzung hat zusätzlich eine eigene `llms-full.txt`, zum Beispiel `/fr/llms-full.txt`. ## Deinen Agenten auf die Doku hinweisen Füge das in die Anweisungen deines Agenten ein, zum Beispiel in `AGENTS.md` oder `CLAUDE.md` im Wurzelverzeichnis deines Projekts: ```md ## MCP servers This project builds MCP servers with picomcp. - Read https://picomcp.github.io/picomcp/llms-full.txt before writing picomcp code. - Define servers with `pico({ name })` and chain `.tool()`, `.resource()`, `.prompt()`. - Use Zod 4 schemas with `.describe()` on every field. - Write tool descriptions and error messages for the model: say what the tool does, when to use it, and what to do after an error. - Never write to stdout in a stdio server. Use `console.error` for logs. - Test tools with `server.call(name, input)`. Do not spawn a client in unit tests. ``` ## Deinem Agenten einen Doku-Server geben Die Doku gibt es auch als MCP-Server, [`picomcp-docs`](https://www.npmjs.com/package/picomcp-docs), gebaut mit picomcp. Er gibt Agenten drei Tools, `list_docs`, `search_docs` und `read_doc`, und jede Seite als Ressource. In Claude Code: ```sh claude mcp add picomcp-docs -- npx -y picomcp-docs ``` In Cursor, Windsurf, Claude Desktop und den meisten anderen Clients trägst du ihn in die MCP-Konfigurationsdatei ein (`.cursor/mcp.json` bei Cursor): ```json { "mcpServers": { "picomcp-docs": { "command": "npx", "args": ["-y", "picomcp-docs"] } } } ``` Der Quellcode liegt in [`packages/docs`](https://github.com/picomcp/picomcp/tree/main/packages/docs), rund 100 Zeilen lang. ## Was Agenten oft falsch machen Das sind die häufigsten Fehler von Modellen beim Schreiben von MCP-Servern, und wie es in picomcp funktioniert: - **Im stdio-Modus ist stdout das Protokoll.** Ein `console.log` würde es beschädigen. picomcp leitet die Konsolenausgabe beim Betrieb über stdio nach stderr um, aber `process.stdout.write` bleibt unsicher. - **Tool-Fehler sind Ergebnisse, keine Exceptions.** Ein Fehler in `run` ergibt `isError: true` mit deiner Meldung. Das Modell sieht sie und kann es erneut versuchen. Fang Fehler nicht ab, nur um `"Error: ..."`-Strings zurückzugeben. - **Prompt-Argumente sind Strings.** Verwende `z.coerce.number()` für numerische Prompt-Argumente. - **Das JSON Schema kommt aus der Schema-Bibliothek.** Zod 4 und ArkType brauchen nichts. Valibot braucht `toStandardJsonSchema`. Andere brauchen `withJsonSchema`. - **`server.fetch` ignoriert den Pfad.** Leite `/mcp` selbst dorthin, oder nutze `serve({ http })`, das nur auf `/mcp` antwortet. - **Typisiertes `server.call` braucht Verkettung.** `pico().tool().tool()` behält die Typen. Getrennte Anweisungen verlieren sie. ## Mit einem Agenten beitragen Das Repository enthält eine [`AGENTS.md`](https://github.com/picomcp/picomcp/blob/main/AGENTS.md) mit den Regeln für die Arbeit an picomcp selbst: das Größenbudget, die Testbefehle und die Designvorgaben.