MCP Server Design
The Seed MCP Server implements the Model Context Protocol using the @modelcontextprotocol/sdk. It provides an HTTP-based transport with session tracking and an extensible tool system.
Overview
The MCP implementation consists of:
- Transport Management: HTTP-based transport with session tracking
- Tool Registration: Centralized system for registering MCP tools
- Prompt Registration: Centralized system for registering MCP prompts
- Resource Registration: Centralized system for registering MCP resources
- Session Lifecycle: UUID-based session identification with Redis-backed metadata
- Request Routing: Session-aware request handling with rate limiting
- Observability: Prometheus metrics and structured logging
Architecture
Transport Management
File: src/mcp/mcp.ts
The transport layer manages HTTP-based MCP communication with session tracking.
StreamableHTTPServerTransport
Seed uses StreamableHTTPServerTransport from @modelcontextprotocol/sdk:
export async function createTransport(): Promise<StreamableHTTPServerTransport> {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true,
onsessioninitialized: (sessionId: string) => {
transports[sessionId] = transport;
},
});
const server = createMcpServer();
await server.connect(transport);
return transport;
}Configuration Options:
sessionIdGenerator: Generates UUIDs (RFC 4122) for session identificationenableJsonResponse: Enables JSON-RPC 2.0 responsesonsessioninitialized: Callback to store transport in session map
Transport Store
interface TransportStore {
[sessionId: string]: StreamableHTTPServerTransport;
}
const transports: TransportStore = {};Hybrid Session Storage:
- In-memory: Map of session IDs to transport instances (not serializable)
- Redis: Session metadata with TTL for distributed validation
- Sessions validated against Redis on each access
- Sliding window TTL refreshed on access via
sessionStore.touch() - Transports cleaned up when connections close or sessions expire
Transport Lifecycle
- Creation: Client sends initialize request without session ID
- Session Generation: Server generates UUID session ID
- Storage: Transport stored in
transportsmap viaonsessioninitialized - Subsequent Requests: Client includes
mcp-session-idheader - Lookup: Server retrieves transport from map
- Cleanup: Transport removed when connection closes (handled by SDK)
MCP Server Creation
Server Configuration
export function createMcpServer(): McpServer {
const server = new McpServer(
{
name: config.server.name, // From package.json
version: config.server.version, // From package.json
},
{
capabilities: {
tools: {}, // Enables tool support
prompts: {}, // Enables prompt support
resources: {}, // Enables resource support
},
},
);
registerTools(server);
registerPrompts(server);
registerResources(server);
return server;
}Server Metadata:
- Name:
seed(from package.json) - Version: Semantic version from package.json
- Capabilities: Supports tools, prompts, and resources
Capability System:
tools: Allows clients to list and call toolsprompts: Allows clients to list and use promptsresources: Allows clients to list and access resources- Future:
sampling(not yet implemented)
Request Routing
File: src/routes/mcp.ts
The MCP route handler manages session tracking and request routing.
Endpoint: POST /mcp
Protected by authentication middleware (requires valid JWT)
Protected by distributed rate limiting (100 req/min per IP, 10,000 globally)
Rate Limiting: See Rate Limiting for comprehensive rate limiting documentation including configuration, implementation details, and observability.
Request Flow
mcpRouter.post("/", async (req: Request, res: Response): Promise<void> => {
// Extract session ID from header
const sessionId = req.headers["mcp-session-id"] as string | undefined;
// Case 1: Existing session
if (sessionId) {
const transport = await getTransport(sessionId); // Validates against Redis
if (transport) {
// Update last accessed time and refresh TTL (sliding window)
const sessionStore = getSessionStore();
await sessionStore.touch(sessionId);
await transport.handleRequest(req, res, req.body);
return;
}
}
// Case 2: New session (initialize request)
if (!sessionId && isInitializeRequest(req.body)) {
const transport = await createTransport();
await transport.handleRequest(req, res, req.body);
return;
}
// Case 3: Invalid request
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Bad Request: No valid session ID provided"
},
id: null
});
});Session Detection
function isInitializeRequest(body: any): boolean {
return (
body &&
body.method === "initialize" &&
body.jsonrpc === "2.0"
);
}An initialize request is identified by:
- JSON-RPC 2.0 format
- Method:
"initialize" - No session ID header
Transport Lookup
export async function getTransport(
sessionId: string
): Promise<StreamableHTTPServerTransport | undefined> {
// Check Redis for session validity first
const sessionStore = getSessionStore();
const sessionMetadata = await sessionStore.get(sessionId);
// If session doesn't exist in Redis or expired, clean up in-memory transport
if (!sessionMetadata) {
delete transports[sessionId];
return undefined;
}
// Session is valid in Redis, return transport
return transports[sessionId];
}Hybrid Validation:
- Check Redis for session metadata (authoritative)
- If expired in Redis → Remove from in-memory, return
undefined - If valid in Redis → Return in-memory transport
- This ensures distributed session validation across multiple server instances
Tool Registration
File: src/mcp/tools/index.ts
Centralized tool registration system.
Registration Function
export function registerTools(server: McpServer): void {
// Basic functionality tools
registerHealthCheck(server);
registerEchoTool(server);
registerUserInfoTool(server);
registerSystemStatusTool(server);
// MCP protocol demonstration tools
registerLogDemoTool(server);
registerTriggerNotificationTool(server);
registerSamplingDemoTool(server);
registerRootsDemoTool(server);
}Design Pattern:
- Each tool has its own file in
src/mcp/tools/ - Each tool exports a registration function
- Registration functions are called centrally
- Tools organized into functional categories
Available Tools:
- healthcheck - Health check tool for MCP connectivity testing
- echo - Simple echo tool for request/response validation
- user-info - Returns authenticated user information from JWT
- system-status - Returns server status and configuration
- log-demo - Demonstrates MCP logging capabilities
- trigger-notification - Demonstrates MCP notification system
- sampling-demo - Demonstrates MCP sampling (LLM completion requests)
- roots-demo - Demonstrates MCP roots (file system access)
Example Tool: echo
File: src/mcp/tools/echo.ts
export function registerEchoTool(server: McpServer): void {
server.tool(
"echo",
"Echo back the provided message",
{
message: {
type: "string",
description: "Message to echo back",
},
},
async (input) => {
return {
content: [
{
type: "text",
text: input.message,
},
],
};
}
);
}Tool Components:
- Name:
"echo"- Tool identifier - Description: Human-readable description for clients
- Schema: JSON Schema for parameters
- Handler: Async function that executes the tool (receives
inputparameter)
Tool Response Format
MCP tools return structured content:
interface ToolResponse {
content: Array<{
type: "text" | "image" | "resource";
text?: string;
data?: string;
mimeType?: string;
}>;
isError?: boolean;
}Content Types:
text: Plain text or markdownimage: Base64-encoded image dataresource: Reference to external resource
Adding New Tools
To add a new tool:
Create tool file:
src/mcp/tools/your_tool.tstypescriptimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export function registerYourTool(server: McpServer): void { server.tool( "your_tool", "Description of your tool", { // Parameter schema param1: { type: "string", description: "Parameter description", }, }, async (params) => { // Tool implementation return { content: [{ type: "text", text: "Result" }], }; } ); }Register tool: Add to
src/mcp/tools/index.tstypescriptimport { registerYourTool } from "./your_tool.js"; export function registerTools(server: McpServer): void { registerSeedPing(server); registerYourTool(server); // Add here }Write tests: Create
src/mcp/tools/your_tool.test.ts
Prompt Registration
File: src/mcp/prompts/index.ts
Centralized prompt registration system. Prompts provide pre-defined message templates that clients can use.
Registration Function
export function registerPrompts(server: McpServer): void {
registerGreetingPrompt(server);
registerUserContextPrompt(server);
}Design Pattern:
- Each prompt has its own file in
src/mcp/prompts/ - Each prompt exports a registration function
- Registration functions are called centrally
- Prompts return message templates for LLM conversations
Available Prompts:
- greeting - Customizable greeting message template with style options (formal, casual, friendly)
- user-context - User authentication context prompt with JWT claims
Example Prompt: greeting
File: src/mcp/prompts/greeting.ts
export function registerGreetingPrompt(server: McpServer): void {
server.registerPrompt(
"greeting",
{
description: "Generate a friendly greeting message. Optionally provide a name to personalize the greeting.",
argsSchema: {
name: z.string().optional().describe("The name of the person to greet"),
style: z.enum(["formal", "casual", "friendly"]).optional().describe("Greeting style (default: friendly)"),
},
},
(args) => {
const name = args.name ?? "there";
const style = args.style ?? "friendly";
let greeting: string;
switch (style) {
case "formal":
greeting = `Good day${name !== "there" ? `, ${name}` : ""}. How may I assist you today?`;
break;
case "casual":
greeting = `Hey${name !== "there" ? ` ${name}` : ""}! What's up?`;
break;
case "friendly":
default:
greeting = `Hello${name !== "there" ? ` ${name}` : ""}! How can I help you today?`;
break;
}
return {
messages: [
{
role: "user",
content: {
type: "text",
text: greeting,
},
},
],
};
}
);
}Prompt Components:
- Name:
"greeting"- Prompt identifier - Description: Human-readable description for clients
- Args Schema: Zod schema for prompt arguments
- Handler: Function that generates message template
Prompt Response Format
MCP prompts return message arrays for LLM conversations:
interface PromptResponse {
messages: Array<{
role: "user" | "assistant" | "system";
content: {
type: "text" | "image" | "resource";
text?: string;
data?: string;
mimeType?: string;
};
}>;
}Message Roles:
user: Messages from the user perspectiveassistant: Messages from the assistant perspectivesystem: System-level instructions
Adding New Prompts
To add a new prompt:
Create prompt file:
src/mcp/prompts/your_prompt.tstypescriptimport type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; export function registerYourPrompt(server: McpServer): void { server.registerPrompt( "your_prompt", { description: "Description of your prompt", argsSchema: { param1: z.string().describe("Parameter description"), }, }, (args) => { return { messages: [ { role: "user", content: { type: "text", text: `Your template with ${args.param1}`, }, }, ], }; } ); }Register prompt: Add to
src/mcp/prompts/index.tstypescriptimport { registerYourPrompt } from "./your_prompt.js"; export function registerPrompts(server: McpServer): void { registerGreetingPrompt(server); registerUserContextPrompt(server); registerYourPrompt(server); // Add here }Write tests: Create
src/mcp/prompts/your_prompt.test.ts
Resource Registration
File: src/mcp/resources/index.ts
Centralized resource registration system. Resources provide read-only data that clients can access.
Registration Function
export function registerResources(server: McpServer): void {
// Static resource examples
registerConfigResource(server);
// Authenticated resource examples
registerUserProfileResource(server);
}Design Pattern:
- Each resource has its own file in
src/mcp/resources/ - Each resource exports a registration function
- Registration functions are called centrally
- Resources are passive and don't perform actions (unlike tools)
Available Resources:
- server-config - Server configuration (name, version, features) at URI
config://server - user-profile - Authenticated user profile from JWT at URI
user://profile
Example Resource: server-config
File: src/mcp/resources/config.ts
export function registerConfigResource(server: McpServer): void {
server.registerResource(
"server-config",
"config://server",
{
title: "Server Configuration",
description: "Read-only server configuration including name, version, and environment settings",
mimeType: "application/json",
},
() => {
// Build safe config object (no secrets)
const safeConfig = {
server: {
name: config.server.name,
version: config.server.version,
},
features: {
authentication: true,
oauth: true,
redis: true,
},
cors: {
enabled: true,
allowedOrigins: config.cors.origin,
},
logging: {
level: config.logging.level,
},
};
return {
contents: [
{
uri: "config://server",
mimeType: "application/json",
text: JSON.stringify(safeConfig, null, 2),
},
],
};
}
);
}Resource Components:
- Name:
"server-config"- Resource identifier - URI:
"config://server"- Unique resource URI - Metadata: Title, description, MIME type
- Handler: Function that returns resource content
Resource Response Format
MCP resources return content arrays:
interface ResourceResponse {
contents: Array<{
uri: string; // Resource URI
mimeType: string; // Content MIME type
text?: string; // Text content
blob?: string; // Base64-encoded binary content
}>;
}MIME Types:
application/json: JSON datatext/plain: Plain texttext/markdown: Markdown contentimage/png,image/jpeg: Images (base64-encoded inblob)
Adding New Resources
To add a new resource:
Create resource file:
src/mcp/resources/your_resource.tstypescriptimport type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export function registerYourResource(server: McpServer): void { server.registerResource( "your-resource", "custom://your-resource", { title: "Your Resource Title", description: "Resource description", mimeType: "application/json", }, () => { const data = { foo: "bar" }; return { contents: [ { uri: "custom://your-resource", mimeType: "application/json", text: JSON.stringify(data, null, 2), }, ], }; } ); }Register resource: Add to
src/mcp/resources/index.tstypescriptimport { registerYourResource } from "./your_resource.js"; export function registerResources(server: McpServer): void { registerConfigResource(server); registerUserProfileResource(server); registerYourResource(server); // Add here }Write tests: Create
src/mcp/resources/your_resource.test.ts
Additional Endpoints
GET /mcp
Returns 405 Method Not Allowed with proper Allow header:
mcpRouter.get("/", (_req: Request, res: Response): void => {
res.status(405).set("Allow", "POST, DELETE").send("Method Not Allowed");
});DELETE /mcp
Explicitly terminates a session:
mcpRouter.delete("/", async (req: Request, res: Response): Promise<void> => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId) {
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Missing Mcp-Session-Id header",
},
id: null,
});
return;
}
const transport = await getTransport(sessionId);
if (transport) {
await removeTransport(sessionId); // Removes from both in-memory and Redis
logger.info("Session deleted", { sessionId });
res.status(204).send();
} else {
res.status(404).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Session not found",
},
id: null,
});
}
});Features:
- Removes session from both in-memory transports and Redis
- Logs session deletion for audit trail
- Returns 204 No Content on success
- Returns 404 if session not found
- Returns 400 if session ID header missing
Session Management
Session ID Format
Session IDs are UUIDs generated using randomUUID():
Example: "123e4567-e89b-12d3-a456-426614174000"Properties:
- RFC 4122 compliant
- Cryptographically random
- 128-bit identifier
- URL-safe representation
Session Header
mcp-session-id: 123e4567-e89b-12d3-a456-426614174000Header Usage:
- Sent by server in response to initialize request
- Included by client in all subsequent requests
- Used for transport lookup in session map
Session Lifecycle
Session Metadata
Session metadata is stored in Redis with the following structure:
interface SessionMetadata {
sessionId: string; // UUID session identifier
createdAt: number; // Unix timestamp when created
lastAccessedAt: number; // Unix timestamp when last accessed
userId?: string; // Optional user ID from JWT
}Metadata Usage:
createdAt: Tracks session agelastAccessedAt: Enables sliding window TTL refreshuserId: Links session to authenticated user for auditing
Session Security
- Sessions are not authenticated separately (rely on JWT middleware)
- Session IDs are not secrets (UUIDs are unpredictable but not secret)
- User context from JWT is available to tools via request object
- Session metadata includes optional
userIdfrom JWT for auditing - Sessions stored in Redis with TTL (default: 24 hours)
- Sliding window: TTL refreshed on each access via
sessionStore.touch()
JSON-RPC 2.0 Protocol
MCP uses JSON-RPC 2.0 for all communication.
Request Format
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "seed_ping",
"arguments": {
"message": "hello"
}
},
"id": 1
}Response Format (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "Seed MCP Server: hello"
}
]
},
"id": 1
}Response Format (Error)
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request",
"data": {
"details": "Additional error information"
}
},
"id": 1
}Common MCP Methods
| Method | Purpose |
|---|---|
initialize | Start new MCP session |
tools/list | List available tools |
tools/call | Execute a tool |
prompts/list | List available prompts |
prompts/get | Get a specific prompt |
resources/list | List available resources |
resources/read | Read a specific resource |
sampling/createMessage | Request LLM completion (not yet implemented) |
Error Handling
Transport Errors
If a transport is not found for a session:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Bad Request: No valid session ID provided"
},
"id": null
}Tool Errors
Tools can return errors using isError flag:
return {
content: [
{
type: "text",
text: "Error: Invalid parameter"
}
],
isError: true
};SDK Errors
The MCP SDK handles protocol-level errors automatically:
- Invalid JSON-RPC format
- Missing required fields
- Invalid method names
- Invalid parameter types
Observability & Monitoring
Prometheus Metrics
MCP operations are tracked with Prometheus metrics:
Session Metrics:
// Active sessions gauge
mcpSessionsActive.inc(); // On session creation
mcpSessionsActive.dec(); // On session termination
// Total sessions counter with status label
mcpSessionsTotal.inc({ status: "created" });
mcpSessionsTotal.inc({ status: "terminated" });Example Queries:
# Current active sessions
mcp_sessions_active
# Session creation rate
rate(mcp_sessions_total{status="created"}[5m])
# Session termination rate
rate(mcp_sessions_total{status="terminated"}[5m])
# Average session lifetime (requires custom metrics)
avg(time() - mcp_session_created_timestamp)Rate Limit Metrics:
# MCP request rate
rate(http_request_rate_limit_requests_total{endpoint="mcp",limited="false"}[5m])
# Rate limit rejections
rate(http_request_rate_limit_requests_total{endpoint="mcp",limited="true"}[5m])
# Global MCP load
sum(rate(http_request_rate_limit_requests_total{endpoint="mcp"}[5m]))Structured Logging
MCP operations are logged with structured data:
Session Creation:
logger.info("Session created", { sessionId });Session Deletion:
logger.info("Session deleted", { sessionId });Session Validation:
logger.debug("Session validated", { sessionId, lastAccessedAt });Tool Execution:
logger.info("Tool executed", {
tool: "echo",
sessionId,
userId,
duration: executionTime
});Error Tracking
MCP errors are logged with context:
Common Error Scenarios:
// Session not found (expired or invalid)
{ error: "Session not found", sessionId: "abc-123", ip: "203.0.113.45" }
// Rate limit exceeded
{ error: "Rate limit exceeded", ip: "203.0.113.45", endpoint: "mcp" }
// Tool execution failure
{ error: "Tool execution failed", tool: "user-info", sessionId: "abc-123", details: message }
// Redis connection failure
{ error: "Session store unavailable", operation: "get", sessionId: "abc-123" }Configuration
Server Metadata
// src/config/index.ts
export const config = {
server: {
name: "seed",
version: "0.1.3", // From package.json
},
};Transport Configuration
No environment configuration required. Transport settings are hardcoded in src/mcp/mcp.ts:
- Session ID generation: UUID v4
- JSON responses: Enabled
- Session storage: In-memory
Testing MCP Tools
Test Structure
// src/mcp/tools/your_tool.test.ts
import { describe, it, expect } from "vitest";
import { createMcpServer } from "../mcp.js";
describe("your_tool", () => {
it("should return expected result", async () => {
const server = createMcpServer();
const result = await server.callTool("your_tool", {
param1: "value",
});
expect(result.content[0].text).toBe("Expected output");
});
it("should handle errors gracefully", async () => {
const server = createMcpServer();
const result = await server.callTool("your_tool", {
invalid: "param",
});
expect(result.isError).toBe(true);
});
});Implementation Files
- Transport:
src/mcp/mcp.ts- Transport creation and session management - Route Handler:
src/routes/mcp.ts- HTTP endpoint with rate limiting - Session Store:
src/services/session-store.ts- Redis-backed session metadata - Tool Registry:
src/mcp/tools/index.ts- Centralized tool registration - Prompt Registry:
src/mcp/prompts/index.ts- Centralized prompt registration - Resource Registry:
src/mcp/resources/index.ts- Centralized resource registration - Example Tools:
src/mcp/tools/*.ts- 8 available tools - Logger Service:
src/services/logger.ts- Winston logger with structured logging - Metrics Service:
src/services/metrics.ts- Prometheus metrics definitions - Rate Limiting:
src/middleware/distributed-rate-limit.ts- Redis-backed rate limiter - Config:
src/config/session.ts,src/config/rate-limit.ts - Tests:
src/mcp/*.test.ts,src/mcp/tools/*.test.ts,src/services/session-store.test.ts
Related Documentation
- Authentication Flow - JWT validation for MCP requests
- Session Management - Detailed session lifecycle
- Configuration System - Server configuration