Tworzenie własnego serwera MCP
Twoja firma ma wewnętrzne API do feature flags, system zarządzania incydentami, którego nikt inny nie używa, oraz niestandardowy pipeline deploymentu zbudowany na Kubernetes. Żaden serwer MCP z marketplace nie obsługuje żadnego z tych systemów. Za każdym razem, gdy AI potrzebuje kontekstu z tych systemów, ręcznie wklejasz odpowiedzi API do chatu. Jesteś ludzkim serwerem MCP i jesteś wąskim gardłem.
Zbudowanie własnego serwera MCP jest prostsze niż się wydaje. SDK MCP obsługuje negocjację protokołu, warstwę transportową i komunikację z klientem. Ty piszesz logikę narzędzi — część, która faktycznie komunikuje się z Twoim API lub systemem — a SDK zajmuje się wszystkim innym.
Co wyniesiesz z tego przewodnika
Dział zatytułowany „Co wyniesiesz z tego przewodnika”- Działający serwer MCP w TypeScript od podstaw, podłączony do wszystkich trzech narzędzi
- Zrozumienie tools, resources i prompts — trzech prymitywów MCP
- Wzorce łączenia się z wewnętrznymi API, bazami danych i narzędziami CLI
- Strategie testowania i debugowania serwerów MCP
- Opcje wdrożenia: lokalny pakiet npm, Docker i zdalne HTTP
Twój pierwszy serwer MCP w 10 minut
Dział zatytułowany „Twój pierwszy serwer MCP w 10 minut”-
Zainicjuj projekt.
Okno terminala mkdir my-mcp-server && cd my-mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zod -
Utwórz serwer. Dodaj to do
index.mjs:#!/usr/bin/env nodeimport { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { z } from 'zod';const server = new Server({name: 'my-first-mcp',version: '1.0.0',});server.tool('get_feature_flags','Returns active feature flags for a given environment',{environment: z.enum(['dev', 'staging', 'production']).describe('Target environment'),},async ({ environment }) => {// Replace with your actual API callconst flags = {dev: { darkMode: true, newCheckout: true, betaSearch: true },staging: { darkMode: true, newCheckout: true, betaSearch: false },production: { darkMode: true, newCheckout: false, betaSearch: false },};return {content: [{type: 'text',text: JSON.stringify(flags[environment], null, 2),}],};});const transport = new StdioServerTransport();await server.connect(transport); -
Nadaj uprawnienia do wykonania.
Okno terminala chmod +x index.mjs -
Podłącz go do swojego edytora.
{ "mcpServers": { "my-server": { "command": "node", "args": ["/absolute/path/to/my-mcp-server/index.mjs"] } }}claude mcp add my-server -- node /absolute/path/to/my-mcp-server/index.mjsLub w .claude/settings.json:
{ "mcpServers": { "my-server": { "command": "node", "args": ["/absolute/path/to/my-mcp-server/index.mjs"] } }}[mcp.my-server]transport = "stdio"command = "node"args = ["/absolute/path/to/my-mcp-server/index.mjs"]Trzy prymitywy MCP
Dział zatytułowany „Trzy prymitywy MCP”Serwery MCP udostępniają trzy typy możliwości:
Tools to funkcje, które AI może wywoływać. Przyjmują ustrukturyzowane dane wejściowe, wykonują operację i zwracają wyniki. Tego będziesz używać najczęściej.
server.tool( 'search_incidents', // Tool name 'Search the incident database', // Description for the AI { // Input schema (Zod) query: z.string(), severity: z.enum(['P1', 'P2', 'P3', 'P4']).optional(), status: z.enum(['open', 'resolved', 'investigating']).optional(), }, async ({ query, severity, status }) => { const results = await searchIncidents({ query, severity, status }); return { content: [{ type: 'text', text: JSON.stringify(results, null, 2), }], }; });Resources
Dział zatytułowany „Resources”Resources to dane, które AI może przeglądać i odczytywać, jak pliki w systemie plików. Używaj ich, gdy chcesz, aby AI odkrywało dostępne dane, zamiast zapytać o konkretne elementy.
server.resource( 'runbooks', 'Operational runbooks for production services', async () => { const runbooks = await listRunbooks(); return runbooks.map(r => ({ uri: `runbook:///${r.id}`, name: r.title, description: r.summary, mimeType: 'text/markdown', })); }, async (uri) => { const id = uri.replace('runbook:///', ''); const content = await getRunbook(id); return { contents: [{ uri, mimeType: 'text/markdown', text: content, }], }; });Prompts
Dział zatytułowany „Prompts”Prompts to wstępnie przygotowane szablony, których AI może używać. Są przydatne do standaryzacji workflow w całym zespole.
server.prompt( 'incident_postmortem', 'Generate a postmortem document for an incident', { incidentId: z.string().describe('Incident ID to generate postmortem for'), }, async ({ incidentId }) => { const incident = await getIncident(incidentId); return { messages: [{ role: 'user', content: { type: 'text', text: `Write a postmortem for incident ${incident.id}: "${incident.title}". Timeline: ${JSON.stringify(incident.timeline)} Root cause: ${incident.rootCause || 'Unknown'} Impact: ${incident.impact}
Follow our postmortem template: title, summary, timeline, root cause analysis, action items with owners and due dates.`, }, }], }; });Wzorce z rzeczywistych zastosowań
Dział zatytułowany „Wzorce z rzeczywistych zastosowań”Opakowywanie wewnętrznego REST API
Dział zatytułowany „Opakowywanie wewnętrznego REST API”const API_BASE = process.env.INTERNAL_API_URL;const API_KEY = process.env.INTERNAL_API_KEY;
if (!API_KEY) { console.error('INTERNAL_API_KEY is required'); process.exit(1);}
async function apiCall(path, options = {}) { const response = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', ...options.headers, }, });
if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); }
return response.json();}
server.tool( 'list_deployments', 'List recent deployments for a service', { service: z.string(), limit: z.number().optional().default(10), }, async ({ service, limit }) => { const deployments = await apiCall(`/deployments?service=${service}&limit=${limit}`); return { content: [{ type: 'text', text: JSON.stringify(deployments, null, 2), }], }; });Opakowywanie narzędzia CLI
Dział zatytułowany „Opakowywanie narzędzia CLI”import { execFile } from 'child_process';import { promisify } from 'util';
const execFileAsync = promisify(execFile);
server.tool( 'kubectl_get', 'Get Kubernetes resources (read-only)', { resource: z.enum(['pods', 'services', 'deployments', 'configmaps', 'ingresses']), namespace: z.string().optional().default('default'), name: z.string().optional(), }, async ({ resource, namespace, name }) => { const args = ['get', resource, '-n', namespace, '-o', 'json']; if (name) args.push(name);
try { const { stdout } = await execFileAsync('kubectl', args, { timeout: 15000 }); return { content: [{ type: 'text', text: stdout, }], }; } catch (error) { return { content: [{ type: 'text', text: `kubectl error: ${error.message}`, }], isError: true, }; } });Testowanie serwera
Dział zatytułowany „Testowanie serwera”Manualne testowanie z MCP Inspector
Dział zatytułowany „Manualne testowanie z MCP Inspector”MCP Inspector to webowe narzędzie do interaktywnego testowania serwerów MCP:
npx @modelcontextprotocol/inspector node index.mjsOtwiera się interfejs przeglądarki, w którym możesz wywoływać poszczególne narzędzia, przeglądać odpowiedzi i debugować problemy bez łączenia się z klientem AI.
Automatyczne testowanie
Dział zatytułowany „Automatyczne testowanie”import { describe, it, expect } from 'vitest';
// Import your tool handler directly for unit testingimport { getFeatureFlags } from '../tools/feature-flags.mjs';
describe('get_feature_flags', () => { it('returns flags for the dev environment', async () => { const result = await getFeatureFlags({ environment: 'dev' }); const parsed = JSON.parse(result.content[0].text); expect(parsed.darkMode).toBe(true); expect(parsed.betaSearch).toBe(true); });
it('returns conservative flags for production', async () => { const result = await getFeatureFlags({ environment: 'production' }); const parsed = JSON.parse(result.content[0].text); expect(parsed.betaSearch).toBe(false); });});Debugowanie
Dział zatytułowany „Debugowanie”Serwery MCP komunikują się przez stdio, więc console.log trafia do transportu i może zepsuć protokół. Zamiast tego używaj console.error do wyjścia debugowania:
const DEBUG = process.env.DEBUG === 'true';
function debug(...args) { if (DEBUG) { console.error('[MCP Debug]', ...args); }}Uruchom z włączonym debugowaniem:
DEBUG=true node index.mjsOpcje wdrożenia
Dział zatytułowany „Opcje wdrożenia”Pakiet npm (zalecane dla zespołów)
Dział zatytułowany „Pakiet npm (zalecane dla zespołów)”Spakuj serwer w celu łatwej instalacji przez npx:
{ "name": "@mycompany/mcp-internal-tools", "version": "1.0.0", "type": "module", "bin": { "mycompany-mcp": "./index.mjs" }}Opublikuj w prywatnym rejestrze npm, a następnie podłącz:
npx -y @mycompany/mcp-internal-toolsDocker (dla izolacji)
Dział zatytułowany „Docker (dla izolacji)”FROM node:22-slimWORKDIR /appCOPY package*.json ./RUN npm ci --productionCOPY . .ENTRYPOINT ["node", "index.mjs"]Zdalny serwer HTTP (dla wspólnego dostępu)
Dział zatytułowany „Zdalny serwer HTTP (dla wspólnego dostępu)”Dla serwerów MCP, do których powinni mieć dostęp wielu deweloperów lub pipeline CI, wdróż jako usługę HTTP z transportem Streamable HTTP:
import { createServer } from 'http';import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamablehttp.js';
const httpServer = createServer(async (req, res) => { if (req.url === '/mcp') { const transport = new StreamableHTTPServerTransport({ req, res }); await server.connect(transport); } else { res.writeHead(404); res.end(); }});
httpServer.listen(3000);Gdy coś nie działa
Dział zatytułowany „Gdy coś nie działa”Serwer uruchamia się, ale nie pojawiają się żadne narzędzia. Rejestracja narzędzi musi zostać zakończona przed połączeniem transportu. Upewnij się, że wszystkie wywołania server.tool() odbywają się przed server.connect(transport).
Błędy “Cannot find module”. Upewnij się, że "type": "module" jest w twoim package.json, gdy używasz składni modułów ES. Lub użyj rozszerzenia .mjs.
Serwer crashuje przy pierwszym wywołaniu narzędzia. Sprawdź, czy funkcje asynchroniczne prawidłowo awaituję swoje promises. Nieobsłużone odrzucenia promise’ów powodują ciche crashe serwera.
AI nie może znaleźć serwera. Zweryfikuj bezwzględną ścieżkę w konfiguracji MCP. Ścieżki względne są rozwiązywane inaczej w zależności od katalogu roboczego edytora.
Timeout przy długich operacjach. Klienci MCP mają domyślne timeouty (zwykle 30-60 sekund). W przypadku długotrwałych operacji najpierw zwróć komunikat o postępie, a następnie końcowy wynik.