The mcp-server/ directory contains the Model Context Protocol (MCP) server implementation, including tools, resources, prompts, and transport layers. This module implements the MCP specification 2025-06-18.
Structure:
- tools/ - Tool definitions and utilities
- resources/ - Resource definitions and utilities
- prompts/ - Prompt definitions and registration
- roots/ - Root directory definitions
- transports/ - HTTP and stdio transport implementations
┌─────────────────────────────────────────────────────┐
│ MCP Client │
│ (Claude Code, etc.) │
└────────────────┬────────────────────────────────────┘
│
│ JSON-RPC 2.0
│
┌────────────────▼────────────────────────────────────┐
│ Transport Layer │
│ (HTTP/SSE or stdio pipes) │
├─────────────────────────────────────────────────────┤
│ MCP Server Instance │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Tools │ │ Resources │ │ Prompts │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Request Handler Factory │ │
│ │ (Tool/Resource/Prompt execution) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────┬──────────────────────────────────┘
│
│ Business Logic
│
┌──────────────────▼──────────────────────────────────┐
│ Services Layer │
│ (LLM, Storage, Graph, etc.) │
└─────────────────────────────────────────────────────┘
File: server.ts
export async function createMcpServerInstance(
context: ModeContext,
): Promise<Server>;Steps:
- Create request context
- Initialize MCP Server with capabilities
- Register tools, resources, and prompts
- Set up lifecycle handlers
- Return configured server instance
Capabilities:
logging- Structured logging supportlistChanged- Dynamic capability updateselicitation- Interactive parameter collectionsampling- LLM integrationprompts- Prompt templatesroots- Root directory access
File: transports/manager.ts
export class TransportManager {
async start(): Promise<void>;
async stop(): Promise<void>;
}Transports:
- HTTP - Server-Sent Events (SSE) with optional auth
- stdio - Standard input/output pipes
Flow:
Client Request
↓
Transport Layer (HTTP/stdio)
↓
MCP Server (route by method)
↓
Handler Factory (createMcpToolHandler/resourceHandlerFactory)
↓
RequestContext + AppContext
↓
Tool/Resource Logic (pure function)
↓
Response Formatter
↓
Client Response
Purpose: Automate discovery and registration of MCP capabilities via DI
The server uses registry services to automatically discover and register tools, resources, prompts, and roots:
Registry Classes:
ToolRegistry- Discovers and registers all tool definitionsResourceRegistry- Discovers and registers all resource definitionsPromptRegistry- Discovers and registers all prompt definitionsRootsRegistry- Enables roots capability for workspace context
How It Works:
- Definition Phase: Create tool/resource/prompt definitions in their respective
definitions/directories - Export Phase: Add definitions to barrel export arrays (
allToolDefinitions,allResourceDefinitions, etc.) - Registration Phase:
registerTools()/registerResources()functions register definitions with DI container - Discovery Phase: Registry services resolve definitions from container via
@injectAll()decorator - Server Phase: Registries call
.registerAll(server)to register with MCP server instance
Example Flow (Tools):
// 1. Define tool
// src/mcp-server/tools/definitions/my-tool.tool.ts
export const myTool: ToolDefinition<...> = { /* ... */ };
// 2. Export in barrel
// src/mcp-server/tools/definitions/index.ts
export const allToolDefinitions = [myTool, ...];
// 3. Register with DI (happens in container setup)
// src/container/registrations/mcp.ts
registerTools(container); // Registers each tool with container
// 4. Registry resolves and registers with server
// src/mcp-server/server.ts
const toolRegistry = container.resolve(ToolRegistry);
await toolRegistry.registerAll(server);Benefits:
- Zero boilerplate: Just export your definition, registration is automatic
- Type safety: Container ensures all definitions match expected types
- Modularity: Easy to add/remove capabilities without touching registration code
- Testability: Registry services can be mocked for testing
Purpose: Executable capabilities that perform actions
Location: tools/definitions/
See: tools/README.md for complete guide
/**
* @fileoverview Echo message tool
* @module mcp-server/tools/definitions/echo
*/
import { z } from 'zod';
import { ToolDefinition } from '../utils/toolDefinition.js';
const TOOL_NAME = 'echo_message' as const;
const InputSchema = z.object({
message: z.string().describe('The message to echo back'),
repeat: z
.number()
.int()
.min(1)
.max(10)
.default(1)
.describe('Number of times to repeat (1-10)'),
});
const OutputSchema = z.object({
result: z.string().describe('The repeated message'),
});
export const echoTool: ToolDefinition<typeof InputSchema, typeof OutputSchema> =
{
name: TOOL_NAME,
description: 'Echoes a message back with optional repetition',
inputSchema: InputSchema,
outputSchema: OutputSchema,
async logic(input, appContext, sdkContext) {
const repeated = Array(input.repeat).fill(input.message).join('\n');
return { result: repeated };
},
responseFormatter: (result) => [{ type: 'text', text: result.result }],
};File: tools/definitions/index.ts
export const allToolDefinitions = [
echoTool,
catFactTool,
// ... add your tools here
] as const;Tools can include optional annotations to provide hints about their behavior:
interface ToolAnnotations {
/** Human-readable title (UI hint) */
title?: string;
/** True if tool does not modify environment (default: false) */
readOnlyHint?: boolean;
/** If not read-only, true if updates can be destructive (default: true) */
destructiveHint?: boolean;
/** If not read-only, true if repeat calls with same args have no additional effect (default: false) */
idempotentHint?: boolean;
/** True if tool may interact with external world like APIs or web search (default: true) */
openWorldHint?: boolean;
}Example:
const TOOL_ANNOTATIONS: ToolAnnotations = {
title: 'Echo Message',
readOnlyHint: true, // Does not modify state
idempotentHint: true, // Same input = same output
openWorldHint: false, // No external API calls
};
export const echoTool: ToolDefinition<typeof InputSchema, typeof OutputSchema> =
{
name: 'echo_message',
annotations: TOOL_ANNOTATIONS,
// ... other fields
};Note: Annotations are hints only and do not provide safety guarantees. Clients should not rely on them for security or correctness.
Purpose: Readable data sources exposed via URI templates
Location: resources/definitions/
See: resources/README.md for complete guide
/**
* @fileoverview Echo resource
* @module mcp-server/resources/definitions/echo
*/
import { z } from 'zod';
import { ResourceDefinition } from '../utils/resourceDefinition.js';
const ParamsSchema = z.object({
message: z.string().optional().describe('The message to echo'),
});
const OutputSchema = z.object({
content: z.string().describe('The echoed message'),
metadata: z
.object({
timestamp: z.string().datetime().describe('ISO 8601 timestamp'),
})
.describe('Response metadata'),
});
export const echoResource: ResourceDefinition<
typeof ParamsSchema,
typeof OutputSchema
> = {
uriTemplate: 'echo://{message}',
name: 'echo',
description: 'A simple resource that echoes back a message',
mimeType: 'text/plain',
paramsSchema: ParamsSchema,
outputSchema: OutputSchema,
// Logic can be sync or async
logic: (uri, params, context) => {
return {
content: `Echo: ${params.message}`,
metadata: { timestamp: new Date().toISOString() },
};
},
};File: resources/definitions/index.ts
export const allResourceDefinitions = [
echoResource,
// ... add your resources here
] as const;Purpose: Reusable prompt templates with variable substitution
Location: prompts/definitions/
/**
* @fileoverview Code review prompt
* @module mcp-server/prompts/definitions/code-review
*/
import { z } from 'zod';
import { PromptDefinition } from '../utils/promptDefinition.js';
const ArgumentsSchema = z.object({
language: z
.string()
.optional()
.describe('Programming language of the code to review'),
focus: z.string().optional().describe('Primary focus area for the review'),
});
export const codeReviewPrompt: PromptDefinition<typeof ArgumentsSchema> = {
name: 'code_review',
description:
'Generates a structured code review prompt for analyzing code quality',
argumentsSchema: ArgumentsSchema,
generate: (args) => {
const focus = args.focus || 'general';
return [
{
role: 'user',
content: {
type: 'text',
text: `You are an expert code reviewer${args.language ? ` specializing in ${args.language}` : ''}. Please conduct a thorough code review with a focus on ${focus}.`,
},
},
];
},
};File: transports/http/httpTransport.ts
Features:
- Server-Sent Events (SSE) for streaming per MCP Spec 2025-06-18
- Session management (stateful/stateless modes)
- Optional authentication (JWT/OAuth) with identity-bound sessions
- CORS support with Origin header validation (DNS rebinding protection)
- Health check endpoint
- OAuth Protected Resource Metadata (RFC 9728)
- MCP-Protocol-Version header validation
Configuration:
MCP_TRANSPORT_TYPE=http
MCP_HTTP_PORT=3000
MCP_HTTP_HOST=0.0.0.0
MCP_HTTP_PATH=/mcp
MCP_ALLOWED_ORIGINS=https://example.com,https://app.example.com
# Session management (stateful or stateless)
MCP_SESSION_MODE=stateful
MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS=3600000 # 1 hourEndpoints:
GET /healthz- Health check (unauthenticated)GET /.well-known/oauth-protected-resource- OAuth metadata per RFC 9728 (unauthenticated)GET /mcp- Server status info (unauthenticated)POST /mcp- JSON-RPC requests (authenticated if auth enabled, returnsMcp-Session-Idheader)DELETE /mcp- Session termination (requiresMcp-Session-Idheader, stateful mode only)OPTIONS /mcp- CORS preflight
Session Management:
The HTTP transport implements MCP Spec 2025-06-18 session management:
-
Stateful mode: Server maintains session state with identity binding
- Sessions are bound to authenticated identity (
tenantId,clientId,subject) - Prevents session hijacking across different authenticated users
- Automatic session expiration after inactivity timeout
- Clients receive
Mcp-Session-Idheader in all responses - Clients MUST send
Mcp-Session-Idheader in subsequent requests - DELETE endpoint explicitly terminates sessions
- Sessions are bound to authenticated identity (
-
Stateless mode: No server-side session state
- Each request is independent
- Session ID still generated for tracing purposes
- DELETE endpoint returns 405 Method Not Allowed
Security Features:
- Origin Header Validation: Protects against DNS rebinding attacks per MCP spec
- MCP-Protocol-Version Validation: Rejects unsupported protocol versions (400 Bad Request)
- Session Identity Binding: Prevents session hijacking in multi-tenant scenarios
- OAuth Discovery: RFC 9728 metadata endpoint for OAuth client discovery
Authentication:
See transports/auth/README.md for details.
# JWT mode
MCP_AUTH_MODE=jwt
MCP_AUTH_SECRET_KEY=your-secret-key
# OAuth mode
MCP_AUTH_MODE=oauth
OAUTH_ISSUER_URL=https://auth.example.com
OAUTH_AUDIENCE=your-app
OAUTH_JWKS_URI=https://auth.example.com/.well-known/jwks.json
MCP_SERVER_RESOURCE_IDENTIFIER=https://api.example.com/mcp # Optional, defaults to audienceFile: transports/stdio/index.ts
Features:
- Standard input/output communication
- No authentication (host-managed)
- Suitable for local MCP clients
Configuration:
MCP_TRANSPORT_TYPE=stdioUsage:
# Development
bun run dev:stdio
# Production
bun run start:stdioEvery request includes context for tracing and multi-tenancy:
interface RequestContext {
requestId: string; // Unique identifier for this request
timestamp: string; // ISO 8601 timestamp
operation: string; // Operation name (e.g., 'HandleToolRequest')
sessionId?: string; // MCP session ID (HTTP transport)
tenantId?: string; // Tenant identifier for multi-tenancy
traceId?: string; // OpenTelemetry trace ID
spanId?: string; // OpenTelemetry span ID
auth?: {
// Authentication info (when auth enabled)
subject: string; // User/service subject (sub claim)
clientId: string; // OAuth client ID
scopes: string[]; // Authorized scopes
token: string; // Raw JWT/OAuth token
tenantId?: string; // Tenant from token (tid claim)
};
// Additional context fields can be added dynamically
[key: string]: unknown;
}Usage in Tools:
async logic(input, appContext, sdkContext) {
const { requestId, sessionId, tenantId } = appContext;
logger.info('Processing request', {
...appContext,
inputSize: JSON.stringify(input).length,
});
// Access auth info if available
if (appContext.auth) {
const { subject, scopes } = appContext.auth;
// Use auth info for authorization checks
}
// Business logic
}File: transports/auth/lib/withAuth.ts
import { withToolAuth } from '@/mcp-server/transports/auth/lib/withAuth.js';
export const myTool: ToolDefinition<typeof InputSchema> = {
name: 'my_tool',
description: 'Protected tool',
inputSchema: InputSchema,
// Wrap logic with auth check
logic: withToolAuth(
['tool:my_tool:execute', 'admin'],
async (input, appContext, sdkContext) => {
// Only executed if user has required scopes
return { success: true };
},
),
};import { withResourceAuth } from '@/mcp-server/transports/auth/lib/withAuth.js';
export const myResource: ResourceDefinition<typeof ParamsSchema> = {
name: 'my_resource',
uriTemplate: 'myresource://{id}',
paramsSchema: ParamsSchema,
logic: withResourceAuth(
['resource:my_resource:read'],
(uri, params, context) => {
// Only executed if user has required scopes
return { data: 'protected' };
},
),
};Session Identity Binding (HTTP Transport):
The HTTP transport binds sessions to authenticated identities to prevent session hijacking in multi-tenant scenarios:
// Session is created with identity binding
const sessionIdentity = {
tenantId: authInfo.tenantId, // From JWT 'tid' claim
clientId: authInfo.clientId, // From JWT 'cid' claim
subject: authInfo.subject, // From JWT 'sub' claim
};
// Session validation checks identity match
if (!sessionStore.isValidForIdentity(sessionId, sessionIdentity)) {
return c.json({ error: 'Session not found or expired' }, 404);
}Security Benefits:
- Prevents session hijacking: User A cannot use User B's session ID
- Multi-tenant isolation: Sessions are tenant-bound
- Audit trail: Session identity tracked for compliance
- Automatic cleanup: Stale sessions expire after timeout
OAuth Discovery (RFC 9728):
The HTTP transport provides OAuth Protected Resource Metadata for client discovery:
GET /.well-known/oauth-protected-resourceResponse:
{
"resource": "https://api.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"],
"resource_signing_alg_values_supported": ["RS256", "ES256", "PS256"],
"resource_documentation": "https://api.example.com/docs",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json"
}Use Cases:
- OAuth client auto-configuration
- Authorization server discovery
- Resource documentation discovery
- JWKS endpoint discovery
Rule: Logic throws, handlers catch
import { McpError, JsonRpcErrorCode } from '@/types-global/errors.js';
async logic(input, appContext, sdkContext) {
// ❌ DO NOT do this in logic:
try {
// ... logic
} catch (error) {
// Handle here
}
// ✅ DO THIS instead:
if (!input.requiredField) {
throw new McpError(
JsonRpcErrorCode.InvalidParams,
'requiredField is required',
{ providedInput: input }
);
}
// Let handler catch errors
const result = await riskyOperation();
return result;
}Error Codes:
InvalidParams(-32602) - Invalid inputInternalError(-32603) - Server errorMethodNotFound(-32601) - Unknown tool/resourceInvalidRequest(-32600) - Malformed request
The sdkContext parameter provides access to MCP SDK capabilities and protocol-level features:
interface SdkContext {
// Request cancellation
signal: AbortSignal; // AbortSignal for request cancellation
// Session info
sessionId?: string; // MCP session ID
// Authentication (if auth enabled)
authInfo?: {
subject: string;
clientId: string;
scopes: string[];
token: string;
tenantId?: string;
};
// Client interaction methods
elicitInput: (args: ElicitInputArgs) => Promise<unknown>;
createMessage: (args: SamplingArgs) => Promise<Message>;
sendNotification: (notification: Notification) => Promise<void>;
sendRequest: (request: Request) => Promise<Response>;
}Interactive collection of missing parameters from the client:
async logic(input, appContext, sdkContext) {
// Check if required param is missing
if (!input.optionalParam) {
// Ask user for it
const response = await sdkContext.elicitInput({
prompt: 'Please provide the optional parameter',
schema: z.object({
optionalParam: z.string(),
}),
});
input.optionalParam = response.optionalParam;
}
return { result: input.optionalParam };
}Request LLM completions from the client per MCP Spec 2025-06-18:
async logic(input, appContext, sdkContext) {
// Create message for LLM
const message = await sdkContext.createMessage({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Analyze: ${input.text}`,
},
},
],
maxTokens: 1000,
});
return { analysis: message.content.text };
}Handle request cancellation gracefully:
async logic(input, appContext, sdkContext) {
// Check if request was cancelled
if (sdkContext.signal.aborted) {
throw new McpError(
JsonRpcErrorCode.RequestCancelled,
'Request was cancelled',
);
}
// Register cancellation handler for long-running operations
sdkContext.signal.addEventListener('abort', () => {
// Clean up resources
});
// Long-running operation
const result = await longOperation();
return result;
}Send notifications to the client without expecting a response:
async logic(input, appContext, sdkContext) {
// Send progress notification
await sdkContext.sendNotification({
method: 'notifications/progress',
params: {
progress: 50,
message: 'Processing...',
},
});
return { result: 'done' };
}responseFormatter: (result) => [{ type: 'text', text: result.message }];import { markdown } from '@/utils/index.js';
responseFormatter: (result) => [
{
type: 'text',
text: markdown()
.heading(1, 'Result')
.paragraph(result.summary)
.codeBlock('json', JSON.stringify(result.data, null, 2))
.build(),
},
];responseFormatter: (result) => [
{
type: 'image',
data: result.imageBase64,
mimeType: 'image/png',
},
];import { describe, it, expect, beforeEach } from 'vitest';
import { myTool } from '@/mcp-server/tools/definitions/my-tool.tool.js';
describe('myTool', () => {
let appContext: AppContext;
let sdkContext: SdkContext;
beforeEach(() => {
appContext = {
requestContext: {
requestId: 'test-req-1',
timestamp: new Date().toISOString(),
operation: 'test',
},
logger,
};
sdkContext = {
elicitInput: vi.fn(),
createMessage: vi.fn(),
};
});
it('executes successfully with valid input', async () => {
const input = { message: 'test' };
const result = await myTool.logic(input, appContext, sdkContext);
expect(result).toBeDefined();
});
it('throws McpError on invalid input', async () => {
const input = { message: '' };
await expect(myTool.logic(input, appContext, sdkContext)).rejects.toThrow(
McpError,
);
});
});All requests are automatically logged with context:
logger.info('Tool execution started', {
tool: TOOL_NAME,
requestId: appContext.requestContext.requestId,
inputSize: JSON.stringify(input).length,
});Automatic instrumentation tracks:
- Request duration
- Success/failure rates
- Input/output payload sizes
- Error codes
Metrics:
mcp_tool_execution_duration_msmcp_tool_execution_successmcp_tool_payload_size_bytes
// ❌ Bad - side effects in logic
async logic(input, appContext, sdkContext) {
console.log('Processing...');
globalState.update(input);
return { result: 'done' };
}
// ✅ Good - pure function
async logic(input, appContext, sdkContext) {
const result = processInput(input);
return { result };
}// ❌ Bad - no descriptions
const InputSchema = z.object({
msg: z.string(),
n: z.number(),
});
// ✅ Good - LLM-friendly descriptions
const InputSchema = z.object({
message: z.string().describe('The message to process'),
count: z
.number()
.int()
.min(1)
.max(10)
.describe('Number of times to repeat (1-10)'),
});// ❌ Bad - swallow errors
async logic(input) {
try {
return await riskyOperation();
} catch {
return { error: 'failed' };
}
}
// ✅ Good - throw McpError
async logic(input) {
try {
return await riskyOperation();
} catch (error) {
throw new McpError(
JsonRpcErrorCode.InternalError,
`Operation failed: ${error instanceof Error ? error.message : 'Unknown'}`,
{ originalError: error }
);
}
}Tools and resources use declarative definitions, not classes. For services, use DI:
// ❌ Bad - direct instantiation in logic
import { StorageService } from '@/storage/core/StorageService.js';
async logic(input, appContext, sdkContext) {
const storage = new StorageService(); // Don't do this!
return await storage.get('key', appContext);
}
// ✅ Good - use DI-injected services from appContext
// Services are injected at server startup and made available through context
import { container } from 'tsyringe';
import { StorageService } from '@/container/tokens.js';
async logic(input, appContext, sdkContext) {
// Resolve service from container
const storage = container.resolve(StorageService);
return await storage.get('key', appContext);
}Declarative Tool Pattern:
// Tools are declarative definitions, not classes
export const myTool: ToolDefinition<typeof InputSchema, typeof OutputSchema> = {
name: 'my_tool',
inputSchema: InputSchema,
outputSchema: OutputSchema,
async logic(input, appContext, sdkContext) {
// Use services via container resolution
const storage = container.resolve(StorageService);
return await storage.get(input.key, appContext);
},
};Error: Tool not found when calling from client
Solution: Ensure tool is exported in tools/definitions/index.ts
Error: Input validation error
Solution: Check Zod schema has proper .describe() calls and matches client input
Error: 401 Unauthorized
Solution:
- Check
MCP_AUTH_MODEconfiguration - Verify JWT/OAuth token is provided
- Check scopes in
withToolAuthwrapper
Error: Cannot connect to server
Solution:
- Verify
MCP_TRANSPORT_TYPEis set correctly - For HTTP: Check
MCP_HTTP_PORTand firewall - For stdio: Check process can spawn correctly
- Tools Module - Tool development guide
- Resources Module - Resource development guide
- Transports Module - Transport configuration
- Services Module - External service integration
- CLAUDE.md - Architectural mandate