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 { 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 call
    const 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);
  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.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 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 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.`,
},
}],
};
}
);
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),
}],
};
}
);
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,
};
}
}
);

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.

test/server.test.mjs
import { describe, it, expect } from 'vitest';
// Import your tool handler directly for unit testing
import { 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);
});
});

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 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);

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.