Budowanie niestandardowych serwerów MCP
Gotowy katalog MCP jest ogromny, ale kończy się na Twoim firewallu. Twoje AI potrafi przeszukać GitHub i odczytać Postgres, lecz nie tknie wewnętrznej wiki, w której zapisana jest każda decyzja architektoniczna, ani autorskiego API do wdrożeń, które rozumie tylko Twój zespół platformowy. W chwili, gdy ktoś z zespołu pyta „dlaczego AI ciągle ignoruje nasz runbook?”, odpowiedź jest zawsze ta sama: nic go z tym runbookiem nie łączy.
Niestandardowy serwer MCP to naprawia. To mały program, który udostępnia Twoje wewnętrzne systemy jako narzędzia, które AI może wywołać — skrobak Confluence, wrapper wokół wewnętrznego API REST, helper do zapytań do zastrzeżonego magazynu danych. Ten przewodnik przeprowadza przez budowę takiego serwera od początku do końca w TypeScript z oficjalnym SDK, a następnie zarejestrowanie go w Cursorze, Claude Code i Codeksie.
Co z tego wyniesiesz
Dział zatytułowany „Co z tego wyniesiesz”- Działający szkielet serwera MCP zbudowany na
@modelcontextprotocol/sdki Zod, rejestrujący prawdziwe narzędzie z walidacją danych wejściowych - Aktualne API
server.registerTool()(stary kształtserver.tool()jest przestarzały od SDK v1.29+) - Dokładną konfigurację łączącą Twój serwer z Cursorem, Claude Code i Codeksem
- Prompty do skopiowania, dzięki którym AI samo zbuduje szkielet serwera i doda dla Ciebie nowe narzędzia
- Listę trybów awarii dla błędów, na które trafia każdy pierwszy serwer MCP (zaśmiecanie stdout, ramkowanie JSON-RPC, walidacja schematu)
Workflow
Dział zatytułowany „Workflow”Nie musisz pisać szablonowego kodu ręcznie. Najszybsza droga to zlecić agentowi AI zbudowanie szkieletu serwera, a potem go przejrzeć i wzmocnić. Zacznij od tego promptu.
1. Skonfiguruj projekt
Dział zatytułowany „1. Skonfiguruj projekt”Zainicjuj projekt Node i zainstaluj SDK, Zod oraz konwerter HTML na Markdown.
npm init -ynpm install @modelcontextprotocol/sdk zod turndownnpm install -D typescript @types/node @types/turndownnpx tsc --init2. Napisz serwer
Dział zatytułowany „2. Napisz serwer”Rejestracja narzędzia to fragment, który większość poradników robi źle. Niskopoziomowe wywołanie server.tool(name, shape, handler) jest przestarzałe w aktualnym SDK — użyj server.registerTool(name, { description, inputSchema }, handler) ze schematem obiektu Zod. Opis to to, co czyta model, decydując, kiedy wywołać narzędzie, więc niech będzie konkretny.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";import TurndownService from "turndown";
const server = new McpServer({ name: "internal-docs", version: "1.0.0" });const turndown = new TurndownService();
server.registerTool( "get_doc", { description: "Fetch an internal Confluence page and return it as Markdown", inputSchema: { url: z.string().url() }, }, async ({ url }) => { try { const response = await fetch(url); if (!response.ok) { return { isError: true, content: [{ type: "text", text: `Fetch failed: HTTP ${response.status}` }], }; } const html = await response.text(); return { content: [{ type: "text", text: turndown.turndown(html) }] }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { isError: true, content: [{ type: "text", text: `Error scraping ${url}: ${message}` }], }; } },);
const transport = new StdioServerTransport();await server.connect(transport);Dwa szczegóły, które gryzą: inputSchema przyjmuje surowy obiekt kształtu Zod ({ url: z.string().url() }), a error jest typowany jako unknown w trybie strict TypeScript, więc musisz go zawęzić (error instanceof Error ? ...), zanim odczytasz .message. Zwrócenie isError: true pozwala modelowi zobaczyć błąd i zareagować, zamiast po cichu dostać pustą treść.
3. Zbuduj go
Dział zatytułowany „3. Zbuduj go”npx tscTo wygeneruje dist/server.js, na który wskazuje każdy z poniższych klientów. Trzymaj console.log z dala od serwera — na transporcie stdio stdout to kanał JSON-RPC, a zabłąkana linia logu uszkadza strumień protokołu. Loguj zamiast tego do stderr (console.error).
Podłącz swój serwer
Dział zatytułowany „Podłącz swój serwer”Cursor i Claude Code czytają ten sam kształt JSON mcpServers, więc te dwie zakładki są celowo identyczne — a jeśli wolisz, Claude Code może zarejestrować serwer jednym poleceniem CLI. Codex używa TOML pod [mcp_servers.<id>], gdzie transport stdio jest implikowany obecnością command (nie ma klucza transport).
Dodaj do .cursor/mcp.json (projekt) lub ~/.cursor/mcp.json (globalnie):
{ "mcpServers": { "internal-docs": { "command": "node", "args": ["/abs/path/to/dist/server.js"] } }}Albo dodaj ten sam JSON do .mcp.json, albo zarejestruj go z terminala:
claude mcp add internal-docs -- node /abs/path/to/dist/server.jsSprawdź poleceniem claude mcp list, a serwer, który się nie ładuje, debuguj przez claude --debug "mcp".
Dodaj do ~/.codex/config.toml:
[mcp_servers.internal-docs]command = "node"args = ["/abs/path/to/dist/server.js"]Klucz transport nie jest potrzebny — stdio jest wnioskowany z command (serwer HTTP użyłby zamiast tego url).
Zrestartuj klienta, a następnie poproś go o wywołanie get_doc na wewnętrznym URL-u, by potwierdzić, że narzędzie jest podpięte.
Dodawanie kolejnych narzędzi
Dział zatytułowany „Dodawanie kolejnych narzędzi”Gdy skrobak tylko do odczytu działa, rozwijaj serwer, dodając narzędzia — każde to kolejne wywołanie registerTool. Pozwól AI rozszerzać własny zestaw narzędzi.
Gdy coś się zepsuje
Dział zatytułowany „Gdy coś się zepsuje”Klient pokazuje serwer jako „failed” bez żadnych narzędzi. Niemal zawsze to problem ze stdout. Każdy console.log, baner czy zależność, która drukuje na stdout, uszkadza strumień JSON-RPC na transporcie stdio. Przenieś całe logowanie do console.error (stderr) i przebuduj.
Błędy parsowania JSON-RPC / serwer się łączy, ale narzędzia nigdy się nie pojawiają. Prawdopodobnie masz przestarzałe SDK lub wciąż wołasz server.tool(). Potwierdź, że npm view @modelcontextprotocol/sdk version to 1.29+ i że przeszedłeś na registerTool. Mieszanie przestarzałego i nowego API w jednym serwerze to częsta przyczyna cichych awarii listy narzędzi.
„Transport mismatch” lub serwer HTTP nigdy nie zostaje osiągnięty. stdio i HTTP konfiguruje się inaczej. Dla stdio klient uruchamia Twój proces przez command/args. Dla zdalnego serwera udostępniasz zamiast tego Streamable HTTP i wskazujesz klientowi url. Nie wstawiaj klucza transport do TOML Codeksa — jest wnioskowany.
Walidacja Zod odrzuca każde wywołanie. Przekaż do inputSchema surowy kształt ({ url: z.string().url() }), a nie gotowy z.object(...), i upewnij się, że opis narzędzia mówi modelowi, czym jest każdy argument — niejasny opis prowadzi do tego, że model wysyła zniekształcone argumenty.
Ścieżki względne zawodzą przy uruchomieniu. Klienci MCP uruchamiają Twój serwer z własnego katalogu roboczego, a nie z korzenia Twojego projektu. Użyj ścieżki bezwzględnej do dist/server.js w każdej konfiguracji powyżej.