Skip to content

Authoring an MCP server

Developing an MCP worker in Speedwave involves writing an isolated microservice process exposing domain capabilities (such as GitLab, Slack, or OS APIs) as Model Context Protocol (MCP) tools. Claude communicates exclusively with the central Tool Gateway, which routes JSON-RPC tool invocations over internal Docker network bridges (http://mcp-{name}:{port}) to the target worker container.

The mcp-servers/gitlab package serves as the canonical reference implementation.

All MCP workers must import @speedwave/mcp-shared rather than implementing custom HTTP bootstrap logic. The package provides:

  • bootWorker: Standardized server initialization lifecycle.
  • Tool: TypeScript interface definition with JSON Schema validation.
  • withClientValidation / withResultValidation: Execution guards for unconfigured or errored states.
  • sanitize: Secret redaction utility for log streams.

Initialize your server entrypoint using bootWorker():

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) => makeStandardHealthCheck(client),
}).catch((error) => {
console.error(`${ts()} Fatal bootstrap error:`, error);
process.exit(1);
});

bootWorker parses PORT allocations dynamically, handles token file validation, and manages retry backoffs.

Workers must mount only scoped secrets at /tokens/ in read-only mode (see Credentials management):

  • Directory/tokens/
    • token API access bearer token
    • host_url Optional custom instance endpoint

If tokens are missing or invalid, initialize the client as null. Handlers will return structured “Service not configured” errors rather than crashing the container.

Define tools using the Tool schema interface:

mcp-servers/gitlab/src/tools/branch-tools.ts
import { Tool, READ_ONLY_ANNOTATIONS } from '@speedwave/mcp-shared';
export const listBranchesTool: Tool = {
name: 'listBranches',
description: 'List branches in a repository',
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 slug' },
search: { type: 'string', description: 'Search term for branch filtering' },
limit: { type: 'number', description: 'Maximum records to return (default 20)' },
},
required: ['project_id'],
},
};

Tool annotations communicate safety boundaries to Claude:

Annotation constantreadOnlyHintdestructiveHintIntended operation type
READ_ONLY_ANNOTATIONStruefalseRead queries (listBranches, getIssue).
WRITE_ANNOTATIONSfalsefalseResource creation/updates (createBranch).
DESTRUCTIVE_ANNOTATIONSfalsetrueIrreversible actions (deleteBranch).

Pair tool declarations with implementation handlers in functional factory modules (createBranchTools(client)):

  • Use withClientValidation when wrapping functions that throw API exceptions.
  • Use withResultValidation when wrapping functions returning structured { success, data, error } result payloads.
  • Filter diagnostic log payloads through sanitize(data) to prevent credential leakage.

Validate tools by invoking factory functions against mocked API adapters using Vitest:

  • Verify metadata schema completeness (names, descriptions, annotations, keywords).
  • Test standard execution with mocked client responses.
  • Test missing or malformed parameter validation.
  • Test error propagation and unconfigured client handling.

Execute make test-mcp and verify 100% test coverage with make coverage-mcp.