Skip to content

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:

typescript
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 identification
  • enableJsonResponse: Enables JSON-RPC 2.0 responses
  • onsessioninitialized: Callback to store transport in session map

Transport Store

typescript
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

  1. Creation: Client sends initialize request without session ID
  2. Session Generation: Server generates UUID session ID
  3. Storage: Transport stored in transports map via onsessioninitialized
  4. Subsequent Requests: Client includes mcp-session-id header
  5. Lookup: Server retrieves transport from map
  6. Cleanup: Transport removed when connection closes (handled by SDK)

MCP Server Creation

Server Configuration

typescript
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 tools
  • prompts: Allows clients to list and use prompts
  • resources: 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

typescript
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

typescript
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

typescript
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:

  1. Check Redis for session metadata (authoritative)
  2. If expired in Redis → Remove from in-memory, return undefined
  3. If valid in Redis → Return in-memory transport
  4. This ensures distributed session validation across multiple server instances

Tool Registration

File: src/mcp/tools/index.ts

Centralized tool registration system.

Registration Function

typescript
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:

  1. healthcheck - Health check tool for MCP connectivity testing
  2. echo - Simple echo tool for request/response validation
  3. user-info - Returns authenticated user information from JWT
  4. system-status - Returns server status and configuration
  5. log-demo - Demonstrates MCP logging capabilities
  6. trigger-notification - Demonstrates MCP notification system
  7. sampling-demo - Demonstrates MCP sampling (LLM completion requests)
  8. roots-demo - Demonstrates MCP roots (file system access)

Example Tool: echo

File: src/mcp/tools/echo.ts

typescript
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:

  1. Name: "echo" - Tool identifier
  2. Description: Human-readable description for clients
  3. Schema: JSON Schema for parameters
  4. Handler: Async function that executes the tool (receives input parameter)

Tool Response Format

MCP tools return structured content:

typescript
interface ToolResponse {
  content: Array<{
    type: "text" | "image" | "resource";
    text?: string;
    data?: string;
    mimeType?: string;
  }>;
  isError?: boolean;
}

Content Types:

  • text: Plain text or markdown
  • image: Base64-encoded image data
  • resource: Reference to external resource

Adding New Tools

To add a new tool:

  1. Create tool file: src/mcp/tools/your_tool.ts

    typescript
    import { 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" }],
          };
        }
      );
    }
  2. Register tool: Add to src/mcp/tools/index.ts

    typescript
    import { registerYourTool } from "./your_tool.js";
    
    export function registerTools(server: McpServer): void {
      registerSeedPing(server);
      registerYourTool(server);  // Add here
    }
  3. 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

typescript
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:

  1. greeting - Customizable greeting message template with style options (formal, casual, friendly)
  2. user-context - User authentication context prompt with JWT claims

Example Prompt: greeting

File: src/mcp/prompts/greeting.ts

typescript
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:

  1. Name: "greeting" - Prompt identifier
  2. Description: Human-readable description for clients
  3. Args Schema: Zod schema for prompt arguments
  4. Handler: Function that generates message template

Prompt Response Format

MCP prompts return message arrays for LLM conversations:

typescript
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 perspective
  • assistant: Messages from the assistant perspective
  • system: System-level instructions

Adding New Prompts

To add a new prompt:

  1. Create prompt file: src/mcp/prompts/your_prompt.ts

    typescript
    import 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}`,
                },
              },
            ],
          };
        }
      );
    }
  2. Register prompt: Add to src/mcp/prompts/index.ts

    typescript
    import { registerYourPrompt } from "./your_prompt.js";
    
    export function registerPrompts(server: McpServer): void {
      registerGreetingPrompt(server);
      registerUserContextPrompt(server);
      registerYourPrompt(server);  // Add here
    }
  3. 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

typescript
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:

  1. server-config - Server configuration (name, version, features) at URI config://server
  2. user-profile - Authenticated user profile from JWT at URI user://profile

Example Resource: server-config

File: src/mcp/resources/config.ts

typescript
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:

  1. Name: "server-config" - Resource identifier
  2. URI: "config://server" - Unique resource URI
  3. Metadata: Title, description, MIME type
  4. Handler: Function that returns resource content

Resource Response Format

MCP resources return content arrays:

typescript
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 data
  • text/plain: Plain text
  • text/markdown: Markdown content
  • image/png, image/jpeg: Images (base64-encoded in blob)

Adding New Resources

To add a new resource:

  1. Create resource file: src/mcp/resources/your_resource.ts

    typescript
    import 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),
              },
            ],
          };
        }
      );
    }
  2. Register resource: Add to src/mcp/resources/index.ts

    typescript
    import { registerYourResource } from "./your_resource.js";
    
    export function registerResources(server: McpServer): void {
      registerConfigResource(server);
      registerUserProfileResource(server);
      registerYourResource(server);  // Add here
    }
  3. Write tests: Create src/mcp/resources/your_resource.test.ts

Additional Endpoints

GET /mcp

Returns 405 Method Not Allowed with proper Allow header:

typescript
mcpRouter.get("/", (_req: Request, res: Response): void => {
  res.status(405).set("Allow", "POST, DELETE").send("Method Not Allowed");
});

DELETE /mcp

Explicitly terminates a session:

typescript
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

http
mcp-session-id: 123e4567-e89b-12d3-a456-426614174000

Header 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:

typescript
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 age
  • lastAccessedAt: Enables sliding window TTL refresh
  • userId: 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 userId from 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

json
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "seed_ping",
    "arguments": {
      "message": "hello"
    }
  },
  "id": 1
}

Response Format (Success)

json
{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Seed MCP Server: hello"
      }
    ]
  },
  "id": 1
}

Response Format (Error)

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32600,
    "message": "Invalid Request",
    "data": {
      "details": "Additional error information"
    }
  },
  "id": 1
}

Common MCP Methods

MethodPurpose
initializeStart new MCP session
tools/listList available tools
tools/callExecute a tool
prompts/listList available prompts
prompts/getGet a specific prompt
resources/listList available resources
resources/readRead a specific resource
sampling/createMessageRequest LLM completion (not yet implemented)

Error Handling

Transport Errors

If a transport is not found for a session:

json
{
  "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:

typescript
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:

typescript
// 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:

promql
# 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:

promql
# 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:

typescript
logger.info("Session created", { sessionId });

Session Deletion:

typescript
logger.info("Session deleted", { sessionId });

Session Validation:

typescript
logger.debug("Session validated", { sessionId, lastAccessedAt });

Tool Execution:

typescript
logger.info("Tool executed", {
  tool: "echo",
  sessionId,
  userId,
  duration: executionTime
});

Error Tracking

MCP errors are logged with context:

Common Error Scenarios:

typescript
// 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

typescript
// 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

typescript
// 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

Released under the MIT License.