Przejdź do głównej zawartości

Napisz serwer MCP

Gdy dodajesz serwer MCP do Speedwave, piszesz worker integracyjny: izolowany proces, który wystawia jedną usługę (GitLab, Slack, system operacyjny) jako narzędzia MCP. Claude nigdy nie rozmawia z twoim workerem bezpośrednio. Zapytania przechodzą przez Tool Gateway, jedyny serwer MCP, który widzi Claude. Gateway nie przechowuje żadnych tokenów i kieruje każde wywołanie przez wewnętrzny DNS Dockera do workera, który jest właścicielem danych uwierzytelniających, pod adresem http://mcp-{name}:{port} (albo http://host.docker.internal:{port} dla workera systemu operacyjnego). Jeśli chcesz udostępnić worker innym instalacjom, spakuj go jako wtyczkę. Zobacz Napisz wtyczkę.

Trzymaj otwarty mcp-servers/gitlab podczas czytania tej strony: to referencyjna implementacja wszystkiego, co opisujemy niżej.

Nie implementuj od nowa uruchamiania, walidacji, formatowania błędów ani helperów wynikowych. @speedwave/mcp-shared już to wszystko ma: bootWorker, typ Tool, wrappery withResultValidation i withClientValidation, wczytywanie tokenów oraz sanitizer logów sanitize.

Wywołaj bootWorker() z main(). Oto cały punkt wejścia workera GitLab:

mcp-servers/gitlab/src/index.ts
import { bootWorker, ts, makeStandardHealthCheck } from '@speedwave/mcp-shared';
import { initializeGitLabClient, type GitLabClient } from './client.js';
import { createToolDefinitions } from './tools/index.js';
bootWorker<GitLabClient>({
serverName: 'mcp-gitlab',
version: '1.0.0',
displayName: 'GitLab',
authTokenEnv: 'MCP_GITLAB_AUTH_TOKEN',
host: '0.0.0.0',
initClient: initializeGitLabClient,
makeTools: (client) => createToolDefinitions(client),
makeHealthCheck: (client) => /* ... */,
}).catch((error) => {
console.error(`${ts()} Fatal error:`, error);
process.exit(1);
});

PORT jest wczytywany automatycznie, do tego dochodzą ponowienia dla initClient z backoffem i wyjście z procesu przy brakującym authTokenEnv. onNotConfigured decyduje, co się stanie, gdy klient wróci nieskonfigurowany: 'warn' (domyślnie: loguje i mimo to startuje) albo 'fail' (kończy proces). Pomiń initClient dla workera bez danych uwierzytelniających, tak jak robi to office.

Montuj wyłącznie dane uwierzytelniające własnej usługi, w /tokens, w trybie tylko do odczytu. Rozwiąż ścieżkę przez tokensDir() i wczytaj plik przez loadTokenFile(name). Klient GitLab czyta dwa pliki:

  • Folder/tokens/
    • token token dostępu GitLab
    • host_url opcjonalny; nadpisuje GITLAB_URL, domyślnie https://gitlab.com

Jeśli wczytywanie się nie powiedzie, złap błąd i zwróć null z funkcji inicjującej zamiast pozwolić na crash: serwer i tak wystartuje, a każde wywołanie narzędzia zwróci błąd „nieskonfigurowany”. Dla usługi opartej na OAuth użyj authedRequest, authedSdkCall i RefreshLock zamiast statycznego tokenu: odświeżają się przy błędzie uwierzytelniania i serializują równoległe odświeżenia.

Narzędzie to obiekt zgodny z interfejsem Tool. Oto listBranches z workera GitLab:

mcp-servers/gitlab/src/tools/branch-tools.ts
const listBranchesTool: Tool = {
name: 'listBranches',
description: 'List branches in a project',
annotations: READ_ONLY_ANNOTATIONS,
_meta: { deferLoading: true },
keywords: ['gitlab', 'branches', 'list', 'git', 'refs'],
example: 'const branches = await gitlab.listBranches({ project_id: "speedwave/core" })',
inputSchema: {
type: 'object',
properties: {
project_id: { type: ['string', 'number'], description: 'Project ID or path' },
search: { type: 'string', description: 'Search by branch name' },
limit: { type: 'number', description: 'Max results (default 20)' },
},
required: ['project_id'],
},
outputSchema: { /* JSON Schema for the result */ },
inputExamples: [
{ description: 'List all branches', input: { project_id: 'my-group/my-project' } },
],
};

Nadaj mu name zgodną z ^[a-zA-Z0-9_-]+$, długości od 1 do 99 znaków, oraz keywords, żeby wykrywanie na żądanie mogło je znaleźć. Ustaw _meta.deferLoading na true dla narzędzia, które ładuje się na żądanie, albo na false, żeby trzymać je stale załadowane. Wybierz stałą adnotacji odpowiednio do tego, co narzędzie robi z usługą:

StałareadOnlyHintdestructiveHintPrzykład
READ_ONLY_ANNOTATIONStruefalselistBranches, getBranch
WRITE_ANNOTATIONSfalsefalsecreateBranch
DESTRUCTIVE_ANNOTATIONSfalsetruedeleteBranch

Połącz każde narzędzie z handlerem w parę { tool, handler }, zgrupuj takie pary w fabryki takie jak createBranchTools(client), a potem zbierz wszystkie fabryki na listę narzędzi swojego workera.

Jeśli twój handler przyjmuje klienta, który nie jest nullem, i rzuca wyjątek przy błędzie, użyj withClientValidation: sprawdza, czy klient nie jest nullem, i mapuje rzucony błąd na komunikat czytelny dla użytkownika, tak jak robi to GitLab. Jeśli twój handler zwraca już wynik w formie { success, data?, error? }, użyj zamiast tego withResultValidation, tak jak robi to Slack. Tak czy inaczej, błędy braku konfiguracji kieruj do notConfiguredMessage(service) i mapuj kody statusu swojej usługi na zrozumiałe komunikaty.

Przepuszczaj wszystko, co logujesz, przez sanitize(), żeby żaden sekret nie trafił do logu.

Wywołaj fabrykę narzędzi z mockiem albo z null jako klientem, a potem znajdź każdy handler po nazwie i wywołaj go bezpośrednio. Sprawdź:

  • Metadane: nazwę, opis, adnotacje, słowa kluczowe, schematy.
  • Ścieżkę udanego wykonania na zamockowanej metodzie klienta.
  • Walidację parametrów dla brakujących albo falsy wartości wejściowych.
  • Obsługę błędów dla rzuconych wyjątków oraz ścieżkę dla nieskonfigurowanego klienta.

Uruchom make test-mcp dla całego zestawu testów i make coverage-mcp dla progów pokrycia. mcp-servers/gitlab/src/tools/ zawiera referencyjne testy, z których możesz kopiować.