Przejdź do głównej zawartości

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.

  • 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
  1. Zainicjuj projekt.

    Okno terminala
    mkdir my-mcp-server && cd my-mcp-server
    npm init -y
    npm install @modelcontextprotocol/sdk zod
  2. Utwórz serwer. Dodaj to do index.mjs:

    #!/usr/bin/env node
    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
    import { z } from 'zod';
    import { getFeatureFlags } from './tools/feature-flags.mjs';
    const server = new McpServer({
    name: 'my-first-mcp',
    version: '1.0.0',
    });
    server.registerTool(
    'get_feature_flags',
    {
    description: 'Returns active feature flags for a given environment',
    inputSchema: {
    environment: z.enum(['dev', 'staging', 'production']).describe('Target environment'),
    },
    },
    async ({ environment }) => ({
    content: [{
    type: 'text',
    text: JSON.stringify(getFeatureFlags({ environment }), null, 2),
    }],
    })
    );
    const transport = new StdioServerTransport();
    await server.connect(transport);

    Trzymaj logikę flag w osobnym module, abyś mógł testować ją jednostkowo bez uruchamiania transportu. Dodaj tools/feature-flags.mjs:

    tools/feature-flags.mjs
    const FLAGS = {
    dev: { darkMode: true, newCheckout: true, betaSearch: true },
    staging: { darkMode: true, newCheckout: true, betaSearch: false },
    production: { darkMode: true, newCheckout: false, betaSearch: false },
    };
    // Replace the lookup below with your actual feature-flag API call.
    export function getFeatureFlags({ environment }) {
    return FLAGS[environment];
    }
  3. Nadaj uprawnienia do wykonania.

    Okno terminala
    chmod +x index.mjs
  4. Podłącz go do swojego edytora.

{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/index.mjs"]
}
}
}

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.registerTool(
'search_incidents', // Tool name
{
description: 'Search the incident database', // Shown to the AI
inputSchema: { // Zod raw shape
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 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.

import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource(
'runbooks',
// A templated URI plus a `list` callback so the AI can browse available runbooks.
new ResourceTemplate('runbook:///{id}', {
list: async () => {
const runbooks = await listRunbooks();
return {
resources: runbooks.map(r => ({
uri: `runbook:///${r.id}`,
name: r.title,
description: r.summary,
mimeType: 'text/markdown',
})),
};
},
}),
{
title: 'Runbooks',
description: 'Operational runbooks for production services',
mimeType: 'text/markdown',
},
// Read handler. The matched `{id}` arrives in the second argument.
async (uri, { id }) => {
const content = await getRunbook(id);
return {
contents: [{
uri: uri.href,
mimeType: 'text/markdown',
text: content,
}],
};
}
);

Prompts to wstępnie przygotowane szablony, których AI może używać. Są przydatne do standaryzacji workflow w całym zespole.

server.registerPrompt(
'incident_postmortem',
{
description: 'Generate a postmortem document for an incident',
argsSchema: {
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.`,
},
}],
};
}
);
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.registerTool(
'list_deployments',
{
description: 'List recent deployments for a service',
inputSchema: {
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),
}],
};
}
);
import { execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
server.registerTool(
'kubectl_get',
{
description: 'Get Kubernetes resources (read-only)',
inputSchema: {
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,
};
}
}
);

MCP Inspector to webowe narzędzie do interaktywnego testowania serwerów MCP:

Okno terminala
npx @modelcontextprotocol/inspector node index.mjs

Otwiera 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.

Ponieważ logika flag żyje w tools/feature-flags.mjs (wyodrębniona w kroku hello-world), możesz testować ją bezpośrednio — bez transportu, bez klienta MCP:

test/feature-flags.test.mjs
import { describe, it, expect } from 'vitest';
// Import the pure handler extracted in tools/feature-flags.mjs
import { getFeatureFlags } from '../tools/feature-flags.mjs';
describe('getFeatureFlags', () => {
it('returns flags for the dev environment', () => {
const flags = getFeatureFlags({ environment: 'dev' });
expect(flags.darkMode).toBe(true);
expect(flags.betaSearch).toBe(true);
});
it('returns conservative flags for production', () => {
const flags = getFeatureFlags({ environment: 'production' });
expect(flags.betaSearch).toBe(false);
});
});

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:

Okno terminala
DEBUG=true node index.mjs

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:

Okno terminala
npx -y @mycompany/mcp-internal-tools
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
ENTRYPOINT ["node", "index.mjs"]

Dla serwerów MCP, do których powinni mieć dostęp wielu deweloperów lub pipeline’y CI, wdróż jako usługę HTTP z transportem Streamable HTTP:

import express from 'express';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
const app = express();
app.use(express.json());
// Stateless transport: a fresh one per request, no session tracking.
// For stateful sessions, pass sessionIdGenerator: () => randomUUID() instead.
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);

Serwer uruchamia się, ale nie pojawiają się żadne narzędzia. Rejestruj narzędzia przed połączeniem transportu. Upewnij się, że wszystkie wywołania server.registerTool() 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.