first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,851 @@
|
||||
# MCP Server
|
||||
|
||||
Model Context Protocol (MCP) server implementation for the n8n McpTrigger node.
|
||||
|
||||
## Overview
|
||||
|
||||
This module provides a clean, modular architecture for handling MCP connections. It separates concerns into distinct layers that can be tested and extended independently.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["MCP Client"]
|
||||
C[Claude Desktop / MCP Client]
|
||||
end
|
||||
|
||||
subgraph McpServer["McpServer (Facade)"]
|
||||
direction TB
|
||||
MS[McpServer]
|
||||
end
|
||||
|
||||
subgraph Layers["Core Layers"]
|
||||
direction TB
|
||||
|
||||
subgraph Session["Session Layer"]
|
||||
SM[SessionManager]
|
||||
SS[(SessionStore)]
|
||||
end
|
||||
|
||||
subgraph Transport["Transport Layer"]
|
||||
TF[TransportFactory]
|
||||
SSE[SSETransport]
|
||||
HTTP[StreamableHttpTransport]
|
||||
end
|
||||
|
||||
subgraph Execution["Execution Layer"]
|
||||
EC[ExecutionCoordinator]
|
||||
DS[DirectStrategy]
|
||||
QS[QueuedStrategy]
|
||||
PM[PendingCallsManager]
|
||||
end
|
||||
|
||||
subgraph Protocol["Protocol Layer"]
|
||||
MP[MessageParser]
|
||||
MF[MessageFormatter]
|
||||
end
|
||||
end
|
||||
|
||||
C <-->|SSE/HTTP| MS
|
||||
MS --> SM
|
||||
MS --> TF
|
||||
MS --> EC
|
||||
MS --> MP
|
||||
MS --> MF
|
||||
SM --> SS
|
||||
TF --> SSE
|
||||
TF --> HTTP
|
||||
EC --> DS
|
||||
EC --> QS
|
||||
QS --> PM
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Module Overview
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| **McpServer** | Main entry point. Coordinates all subsystems. |
|
||||
| **Session** | Manages client connections and tool registrations. |
|
||||
| **Transport** | Handles communication protocols (SSE, Streamable HTTP). |
|
||||
| **Execution** | Executes tools directly or via worker queue. |
|
||||
| **Protocol** | Parses and formats MCP messages. |
|
||||
|
||||
### McpServer Facade
|
||||
|
||||
The `McpServer` class is the **main entry point** for all MCP operations. It implements the [Facade pattern](https://refactoring.guru/design-patterns/facade), providing a simplified interface that coordinates all the underlying subsystems.
|
||||
|
||||
#### Why a Facade?
|
||||
|
||||
Without the facade, consumers would need to:
|
||||
1. Create and configure a SessionManager with a SessionStore
|
||||
2. Create a TransportFactory
|
||||
3. Create transports and wire up event handlers
|
||||
4. Create an ExecutionCoordinator with a strategy
|
||||
5. Parse incoming messages with MessageParser
|
||||
6. Format responses with MessageFormatter
|
||||
7. Wire everything together correctly
|
||||
|
||||
The `McpServer` facade handles all this complexity internally, exposing just a few high-level methods.
|
||||
|
||||
#### Singleton Pattern
|
||||
|
||||
```typescript
|
||||
const mcpServer = McpServer.instance(logger);
|
||||
```
|
||||
|
||||
`McpServer` is a **singleton** - only one instance exists per process. This ensures:
|
||||
- All MCP requests share the same session registry
|
||||
- Pending responses are tracked in one place
|
||||
- Configuration changes (session store, execution strategy) apply globally
|
||||
|
||||
#### Request Flow Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Incoming["Incoming Requests"]
|
||||
GET["GET /sse (SSE setup)"]
|
||||
POST_INIT["POST /mcp (Streamable HTTP init)"]
|
||||
POST_MSG["POST /messages (tool call)"]
|
||||
DELETE["DELETE /mcp (session close)"]
|
||||
end
|
||||
|
||||
subgraph McpServer["McpServer Facade"]
|
||||
HandleSetup[handleSetupRequest]
|
||||
HandleStreamable[handleStreamableHttpSetup]
|
||||
HandlePost[handlePostMessage]
|
||||
HandleDelete[handleDeleteRequest]
|
||||
HandleWorker[handleWorkerResponse]
|
||||
StorePending[storePendingResponse]
|
||||
end
|
||||
|
||||
subgraph Internal["Internal Coordination"]
|
||||
CreateServer[createServer]
|
||||
SetupSession[setupSession]
|
||||
SetupHandlers[setupHandlers]
|
||||
CleanupSession[cleanupSession]
|
||||
RecreateTransport[recreateStreamableHttpTransport]
|
||||
end
|
||||
|
||||
GET --> HandleSetup
|
||||
POST_INIT --> HandleStreamable
|
||||
POST_MSG --> HandlePost
|
||||
DELETE --> HandleDelete
|
||||
|
||||
HandleSetup --> CreateServer
|
||||
HandleSetup --> SetupSession
|
||||
HandleStreamable --> CreateServer
|
||||
HandleStreamable --> SetupHandlers
|
||||
HandlePost --> RecreateTransport
|
||||
HandleDelete --> CleanupSession
|
||||
|
||||
SetupSession --> SetupHandlers
|
||||
```
|
||||
|
||||
#### Public Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `instance(logger)` | Get the singleton instance |
|
||||
| `handleSetupRequest(req, resp, serverName, postUrl, tools)` | Handle SSE connection setup (GET request) |
|
||||
| `handleStreamableHttpSetup(req, resp, serverName, tools)` | Handle Streamable HTTP initialization (POST with `initialize` method) |
|
||||
| `handlePostMessage(req, resp, tools, serverName?)` | Handle incoming tool calls or list-tools requests. Returns `HandlePostResult` |
|
||||
| `handleDeleteRequest(req, resp)` | Handle session termination |
|
||||
| `handleWorkerResponse(sessionId, messageId, result)` | Route worker results back to clients (queue mode) |
|
||||
| `storePendingResponse(sessionId, messageId)` | Track a pending response awaiting worker result |
|
||||
| `hasPendingResponse(sessionId, messageId)` | Check if a pending response exists |
|
||||
| `removePendingResponse(sessionId, messageId)` | Remove a pending response |
|
||||
| `pendingResponseCount` | Getter for the number of pending responses |
|
||||
| `getMcpMetadata(req)` | Extract session ID and message ID from a request |
|
||||
| `getSessionId(req)` | Extract session ID from query string or header |
|
||||
| `getTransport(sessionId)` | Get the transport for a session |
|
||||
| `getTools(sessionId)` | Get the tools registered for a session |
|
||||
|
||||
#### HandlePostResult Type
|
||||
|
||||
The `handlePostMessage` method returns a `HandlePostResult` object:
|
||||
|
||||
```typescript
|
||||
interface HandlePostResult {
|
||||
wasToolCall: boolean; // Whether the request was a tool call
|
||||
toolCallInfo?: McpToolCallInfo; // Info about the tool call (if any)
|
||||
messageId?: string; // The JSONRPC message ID
|
||||
relaySessionId?: string; // Session ID for relayed requests (queue mode)
|
||||
needsListToolsRelay?: boolean; // Whether this is a list-tools request needing relay
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuration Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `setSessionStore(store)` | Replace the session store (e.g., InMemory → Redis) |
|
||||
| `setExecutionStrategy(strategy)` | Replace the execution strategy (e.g., Direct → Queued) |
|
||||
| `isQueueMode()` | Check if using queued execution |
|
||||
| `getPendingCallsManager()` | Get the pending calls manager (needed for QueuedExecutionStrategy) |
|
||||
|
||||
#### Internal Coordination
|
||||
|
||||
The facade coordinates these internal operations:
|
||||
|
||||
| Internal Method | What It Does |
|
||||
|-----------------|--------------|
|
||||
| `createServer(serverName)` | Creates an MCP SDK `Server` instance with capabilities |
|
||||
| `setupSession(server, transport, tools, resp)` | Registers session, sets up close handlers, connects server to transport |
|
||||
| `setupHandlers(server)` | Registers `tools/list` and `tools/call` request handlers on the MCP server |
|
||||
| `cleanupSession(sessionId)` | Cleans up pending calls, pending responses, and destroys the session |
|
||||
| `recreateStreamableHttpTransport(...)` | Recreates a transport for an existing session (multi-instance scenarios) |
|
||||
|
||||
#### Queue Mode Behavior
|
||||
|
||||
In queue mode (multi-instance deployment), the facade has additional responsibilities:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Main as McpServer (Main)
|
||||
participant Redis
|
||||
participant Worker
|
||||
|
||||
Client->>Main: POST /messages (tool call)
|
||||
Main->>Main: storePendingResponse()
|
||||
Main-->>Client: 202 Accepted
|
||||
Main->>Redis: Enqueue job
|
||||
|
||||
Redis->>Worker: Dequeue job
|
||||
Worker->>Worker: Execute tool
|
||||
Worker->>Redis: Publish result
|
||||
|
||||
Redis->>Main: mcp-response event
|
||||
Main->>Main: handleWorkerResponse()
|
||||
Main-->>Client: Result via SSE/HTTP
|
||||
```
|
||||
|
||||
Key queue mode methods:
|
||||
- **`storePendingResponse()`** - Tracks that we're waiting for a worker result
|
||||
- **`handleWorkerResponse()`** - Routes the worker's result back to the correct client
|
||||
- **`hasPendingResponse()`** / **`removePendingResponse()`** - Manage pending response state
|
||||
|
||||
## Layers
|
||||
|
||||
### 1. Protocol Layer
|
||||
|
||||
The Protocol layer handles the translation between raw HTTP request bodies and strongly-typed MCP data structures. MCP uses [JSONRPC 2.0](https://www.jsonrpc.org/specification) as its wire protocol, so every message from an MCP client is a JSONRPC request.
|
||||
|
||||
#### Why This Layer Exists
|
||||
|
||||
When an MCP client sends a request (e.g., "call tool X with arguments Y"), it arrives as a raw JSON string in the HTTP request body. The Protocol layer:
|
||||
|
||||
1. **Parses and validates** the raw JSON against the JSONRPC schema
|
||||
2. **Identifies the request type** (tool call, list tools, etc.)
|
||||
3. **Extracts the relevant data** (tool name, arguments) into typed structures
|
||||
4. **Formats responses** back into the MCP-expected format
|
||||
|
||||
This keeps the rest of the codebase working with clean, typed data instead of raw JSON.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Incoming["Incoming Request"]
|
||||
Raw["Raw JSON Body<br/>{ jsonrpc: '2.0', method: 'tools/call', ... }"]
|
||||
end
|
||||
|
||||
subgraph MessageParser["MessageParser"]
|
||||
Parse[parse]
|
||||
IsToolCall[isToolCall]
|
||||
IsListTools[isListToolsRequest]
|
||||
GetId[getRequestId]
|
||||
Extract[extractToolCallInfo]
|
||||
end
|
||||
|
||||
subgraph Outgoing["Outgoing Response"]
|
||||
Result["Tool Execution Result<br/>(string, object, Error)"]
|
||||
end
|
||||
|
||||
subgraph MessageFormatter["MessageFormatter"]
|
||||
FormatResult[formatToolResult]
|
||||
FormatError[formatError]
|
||||
end
|
||||
|
||||
Raw --> Parse
|
||||
Parse --> IsToolCall
|
||||
Parse --> IsListTools
|
||||
Parse --> GetId
|
||||
Parse --> Extract
|
||||
Extract --> Info["McpToolCallInfo<br/>{ toolName, arguments }"]
|
||||
|
||||
Result --> FormatResult
|
||||
Result --> FormatError
|
||||
FormatResult --> McpResult["McpToolResult<br/>{ content: [{ type, text }] }"]
|
||||
FormatError --> McpResult
|
||||
```
|
||||
|
||||
#### Types
|
||||
|
||||
```typescript
|
||||
// Extracted info from a tool call request
|
||||
interface McpToolCallInfo {
|
||||
toolName: string; // Name of the tool to invoke
|
||||
arguments: Record<string, unknown>; // Arguments passed to the tool
|
||||
sourceNodeName?: string; // Optional: n8n node that registered the tool
|
||||
}
|
||||
|
||||
// Formatted result to send back to the client
|
||||
interface McpToolResult {
|
||||
content: Array<{ type: string; text: string }>; // MCP content blocks
|
||||
isError?: boolean; // Flag for error responses
|
||||
}
|
||||
|
||||
// Special marker returned when handling list-tools requests
|
||||
const MCP_LIST_TOOLS_REQUEST_MARKER = { _listToolsRequest: true };
|
||||
```
|
||||
|
||||
#### MessageParser Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `parse(body)` | Parses a raw JSON string into a validated `JSONRPCMessage`. Returns `undefined` if invalid. |
|
||||
| `isToolCall(body)` | Returns `true` if the message is a `tools/call` request (client wants to invoke a tool) |
|
||||
| `isListToolsRequest(body)` | Returns `true` if the message is a `tools/list` request (client wants to discover available tools) |
|
||||
| `getRequestId(message)` | Extracts the JSONRPC request ID (needed to correlate responses with requests) |
|
||||
| `extractToolCallInfo(body)` | Extracts the tool name and arguments from a tool call request into `McpToolCallInfo` |
|
||||
|
||||
#### MessageFormatter Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `formatToolResult(result)` | Converts a tool's return value (string, object, etc.) into an `McpToolResult` with proper content blocks |
|
||||
| `formatError(error)` | Converts an Error into an `McpToolResult` with `isError: true` and the error message |
|
||||
|
||||
#### Example Flow
|
||||
|
||||
```typescript
|
||||
// 1. Client sends a tool call
|
||||
const body = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"London"}}}';
|
||||
|
||||
// 2. Parse and identify
|
||||
MessageParser.isToolCall(body); // true
|
||||
MessageParser.isListToolsRequest(body); // false
|
||||
|
||||
// 3. Extract tool info
|
||||
const info = MessageParser.extractToolCallInfo(body);
|
||||
// { toolName: 'get_weather', arguments: { city: 'London' } }
|
||||
|
||||
// 4. Execute tool and format result
|
||||
const result = await executeTool(info); // { temperature: 15, unit: 'celsius' }
|
||||
const formatted = MessageFormatter.formatToolResult(result);
|
||||
// { content: [{ type: 'text', text: '{"temperature":15,"unit":"celsius"}' }] }
|
||||
|
||||
// 5. Or format an error
|
||||
const error = MessageFormatter.formatError(new Error('City not found'));
|
||||
// { isError: true, content: [{ type: 'text', text: 'Error: City not found' }] }
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `types.ts` - Type definitions (`McpToolCallInfo`, `McpToolResult`, `MCP_LIST_TOOLS_REQUEST_MARKER`)
|
||||
- `MessageParser.ts` - Parses raw JSON, identifies request types, extracts tool call info
|
||||
- `MessageFormatter.ts` - Formats tool results and errors for MCP responses
|
||||
|
||||
### 2. Session Layer
|
||||
|
||||
The Session layer manages MCP client connections and their associated state (tools, transport, server instance). Each MCP client establishes a session when connecting, and that session persists for the lifetime of the connection.
|
||||
|
||||
#### Why Sessions Are Needed
|
||||
|
||||
MCP uses a stateful protocol where:
|
||||
1. A client connects and establishes a session (via SSE or Streamable HTTP)
|
||||
2. The client can then make multiple tool calls within that session
|
||||
3. Each session has its own transport (for sending responses back) and set of available tools
|
||||
|
||||
Sessions allow the server to:
|
||||
- Track which clients are connected
|
||||
- Route responses back to the correct client
|
||||
- Associate tools with specific client connections
|
||||
- Validate that incoming requests belong to active sessions
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph SessionManager["SessionManager (Coordinator)"]
|
||||
direction LR
|
||||
Register[registerSession]
|
||||
Destroy[destroySession]
|
||||
GetSession[getSession]
|
||||
GetTransport[getTransport]
|
||||
GetServer[getServer]
|
||||
IsValid[isSessionValid]
|
||||
Tools[getTools / setTools]
|
||||
end
|
||||
|
||||
subgraph InMemoryState["In-Memory State (SessionManager)"]
|
||||
SessionInfo["sessions: Record<sessionId, SessionInfo>"]
|
||||
SI_Content["SessionInfo = { sessionId, server, transport }"]
|
||||
end
|
||||
|
||||
subgraph SessionStore["SessionStore Interface"]
|
||||
InMemory[InMemorySessionStore]
|
||||
Redis[RedisSessionStore]
|
||||
end
|
||||
|
||||
SessionManager --> InMemoryState
|
||||
SessionManager --> SessionStore
|
||||
InMemory -.->|implements| SessionStore
|
||||
Redis -.->|implements| SessionStore
|
||||
```
|
||||
|
||||
#### Two-Level Storage Architecture
|
||||
|
||||
The Session layer uses a two-level storage architecture:
|
||||
|
||||
| Storage Level | What It Stores | Why |
|
||||
|---------------|----------------|-----|
|
||||
| **SessionManager (in-memory)** | `SessionInfo` objects containing the MCP `Server` instance and `Transport` | These are runtime objects (WebSocket connections, SSE streams) that cannot be serialized or shared across processes |
|
||||
| **SessionStore (pluggable)** | Session IDs (for validation) and Tools array | Can be backed by Redis for multi-instance deployments where sessions need to be validated across workers |
|
||||
|
||||
This separation allows:
|
||||
- **Single-instance mode**: Use `InMemorySessionStore` (default) - everything stays in process memory
|
||||
- **Multi-instance/queue mode**: Use `RedisSessionStore` - session validation and tools can be checked by any worker, while the actual transport/server objects remain on the main instance that holds the client connection
|
||||
|
||||
#### SessionStore Interface
|
||||
|
||||
```typescript
|
||||
interface SessionStore {
|
||||
register(sessionId: string): Promise<void>; // Register a new session
|
||||
validate(sessionId: string): Promise<boolean>; // Check if session exists
|
||||
unregister(sessionId: string): Promise<void>; // Remove a session
|
||||
getTools(sessionId: string): Tool[] | undefined; // Get tools for session
|
||||
setTools(sessionId: string, tools: Tool[]): void; // Associate tools with session
|
||||
clearTools(sessionId: string): void; // Remove tools from session
|
||||
}
|
||||
```
|
||||
|
||||
#### SessionManager Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `registerSession(sessionId, server, transport, tools?)` | Called when a new client connects. Stores the session info in memory and registers with the SessionStore |
|
||||
| `destroySession(sessionId)` | Called when a client disconnects. Cleans up both in-memory state and SessionStore |
|
||||
| `getSession(sessionId)` | Returns the full `SessionInfo` (sessionId, server, transport) |
|
||||
| `getTransport(sessionId)` | Returns just the transport for sending responses back to the client |
|
||||
| `getServer(sessionId)` | Returns the MCP Server instance for this session |
|
||||
| `isSessionValid(sessionId)` | Delegates to SessionStore to check if session exists (useful in multi-instance setups) |
|
||||
| `getTools(sessionId)` / `setTools(sessionId, tools)` | Manage the tools available for this session |
|
||||
| `setStore(store)` / `getStore()` | Swap the SessionStore implementation (e.g., from InMemory to Redis) |
|
||||
|
||||
**Files:**
|
||||
- `SessionStore.ts` - Interface for session storage
|
||||
- `InMemorySessionStore.ts` - Default in-memory implementation (uses `Set` for sessions, `Record` for tools)
|
||||
- `SessionManager.ts` - Coordinates session operations, holds runtime objects
|
||||
|
||||
### 3. Transport Layer
|
||||
|
||||
The Transport layer abstracts the communication protocol between the MCP server and clients. MCP supports multiple transport mechanisms, and this layer provides a unified interface so the rest of the code doesn't need to know which protocol is being used.
|
||||
|
||||
#### Why This Layer Exists
|
||||
|
||||
MCP clients can connect using different protocols:
|
||||
- **SSE (Server-Sent Events)** - A long-lived HTTP connection where responses stream back to the client
|
||||
- **Streamable HTTP** - Request-response based with optional streaming, more REST-like
|
||||
|
||||
Each protocol has different characteristics, but the server logic (handling tool calls, managing sessions) should be the same regardless. The Transport layer provides:
|
||||
1. A **common interface** (`McpTransport`) that both protocols implement
|
||||
2. A **factory** to create the right transport type
|
||||
3. **Protocol-specific wrappers** that handle the differences internally
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["MCP Client"]
|
||||
C[Claude Desktop / MCP Client]
|
||||
end
|
||||
|
||||
subgraph TransportLayer["Transport Layer"]
|
||||
subgraph McpTransport["McpTransport Interface"]
|
||||
Send[send]
|
||||
HandleReq[handleRequest]
|
||||
Close[close]
|
||||
end
|
||||
|
||||
subgraph Implementations["Implementations"]
|
||||
SSE[SSETransport]
|
||||
HTTP[StreamableHttpTransport]
|
||||
end
|
||||
|
||||
subgraph Factory["TransportFactory"]
|
||||
CreateSSE[createSSE]
|
||||
CreateHTTP[createStreamableHttp]
|
||||
Recreate[recreateStreamableHttp]
|
||||
end
|
||||
end
|
||||
|
||||
C <-->|"GET /sse + POST /messages"| SSE
|
||||
C <-->|"POST /mcp"| HTTP
|
||||
SSE -.->|implements| McpTransport
|
||||
HTTP -.->|implements| McpTransport
|
||||
Factory --> SSE
|
||||
Factory --> HTTP
|
||||
```
|
||||
|
||||
#### McpTransport Interface
|
||||
|
||||
```typescript
|
||||
interface McpTransport {
|
||||
readonly transportType: 'sse' | 'streamableHttp'; // Identifies the transport type
|
||||
readonly sessionId: string | undefined; // Session ID for this connection
|
||||
|
||||
send(message: JSONRPCMessage): Promise<void>; // Send a message to the client
|
||||
handleRequest(req, resp, body?): Promise<void>; // Handle an incoming request
|
||||
close?(): Promise<void>; // Close the transport
|
||||
|
||||
onclose?: () => void | Promise<void>; // Callback when connection closes
|
||||
}
|
||||
```
|
||||
|
||||
#### SSE Transport
|
||||
|
||||
**Server-Sent Events** is a unidirectional streaming protocol where:
|
||||
1. Client opens a long-lived GET connection to `/sse`
|
||||
2. Server keeps the connection open and streams events (responses) back
|
||||
3. Client sends tool calls via separate POST requests to `/messages`
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Server
|
||||
|
||||
Client->>Server: GET /sse
|
||||
Note over Server: Connection stays open
|
||||
Server-->>Client: SSE: endpoint event (POST URL)
|
||||
|
||||
Client->>Server: POST /messages (tool call)
|
||||
Server-->>Client: SSE: message event (result)
|
||||
|
||||
Client->>Server: POST /messages (another call)
|
||||
Server-->>Client: SSE: message event (result)
|
||||
|
||||
Note over Client,Server: Connection persists until closed
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Long-lived connection (held open for the session lifetime)
|
||||
- Responses stream back on the same connection
|
||||
- Tool calls arrive via separate POST requests
|
||||
- Session ID passed as query parameter (`?sessionId=...`)
|
||||
- Good for real-time, continuous interactions
|
||||
|
||||
**Implementation:** `SSETransport` extends the MCP SDK's `SSEServerTransport` and:
|
||||
- Adds the `McpTransport` interface
|
||||
- Flushes the response after each send (for compression middleware compatibility)
|
||||
|
||||
#### Streamable HTTP Transport
|
||||
|
||||
**Streamable HTTP** is a request-response protocol where:
|
||||
1. Client sends POST requests to `/mcp`
|
||||
2. Each request can optionally stream responses back
|
||||
3. Session continuity via `mcp-session-id` header
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Server
|
||||
|
||||
Client->>Server: POST /mcp (initialize)
|
||||
Server-->>Client: Response + mcp-session-id header
|
||||
|
||||
Client->>Server: POST /mcp (tool call)<br/>Header: mcp-session-id
|
||||
Server-->>Client: Response (result)
|
||||
|
||||
Client->>Server: DELETE /mcp<br/>Header: mcp-session-id
|
||||
Server-->>Client: 200 OK (session closed)
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Request-response based (more REST-like)
|
||||
- Session ID passed via `mcp-session-id` header
|
||||
- Supports session recreation on different server instances
|
||||
- Better for stateless/load-balanced deployments
|
||||
|
||||
**Implementation:** `StreamableHttpTransport` extends the MCP SDK's `StreamableHTTPServerTransport` and:
|
||||
- Adds the `McpTransport` interface
|
||||
- Provides `markAsInitialized()` for recreating transports with existing sessions
|
||||
- Flushes responses for compression compatibility
|
||||
|
||||
#### TransportFactory
|
||||
|
||||
The factory creates transport instances with the right configuration:
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `createSSE(postUrl, response)` | Creates an SSE transport. `postUrl` is the URL clients should POST tool calls to. |
|
||||
| `createStreamableHttp(options, response)` | Creates a Streamable HTTP transport with session initialization callbacks. |
|
||||
| `recreateStreamableHttp(sessionId, response)` | Recreates a transport for an existing session (multi-instance scenarios). |
|
||||
|
||||
#### Transport Recreation (Multi-Instance)
|
||||
|
||||
In multi-instance deployments, a client might have established a session on Instance A, but a subsequent request lands on Instance B. The `recreateStreamableHttp()` method handles this:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant InstanceA as Instance A
|
||||
participant InstanceB as Instance B
|
||||
participant Redis
|
||||
|
||||
Client->>InstanceA: POST /mcp (initialize)
|
||||
InstanceA->>Redis: Register session
|
||||
InstanceA-->>Client: mcp-session-id: abc123
|
||||
|
||||
Note over Client,InstanceB: Load balancer routes to different instance
|
||||
|
||||
Client->>InstanceB: POST /mcp (tool call)<br/>Header: mcp-session-id: abc123
|
||||
InstanceB->>Redis: Validate session exists
|
||||
InstanceB->>InstanceB: recreateStreamableHttp(abc123)
|
||||
InstanceB-->>Client: Response
|
||||
```
|
||||
|
||||
The recreated transport is marked as already initialized (via `markAsInitialized()`) so it skips the initialization handshake.
|
||||
|
||||
#### CompressionResponse Type
|
||||
|
||||
```typescript
|
||||
type CompressionResponse = Response & {
|
||||
flush?: () => void;
|
||||
};
|
||||
```
|
||||
|
||||
This type extends Express's `Response` to include an optional `flush()` method. When using compression middleware (like `compression`), responses are buffered. Calling `flush()` forces buffered data to be sent immediately - important for SSE where responses need to arrive in real-time.
|
||||
|
||||
**Files:**
|
||||
- `Transport.ts` - `McpTransport` interface and `CompressionResponse` type
|
||||
- `SSETransport.ts` - SSE implementation wrapping MCP SDK's `SSEServerTransport`
|
||||
- `StreamableHttpTransport.ts` - Streamable HTTP implementation wrapping MCP SDK's `StreamableHTTPServerTransport`
|
||||
- `TransportFactory.ts` - Factory for creating transport instances
|
||||
|
||||
### 4. Execution Layer
|
||||
|
||||
Implements strategy pattern for tool execution, allowing different execution modes depending on deployment scenario.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph ExecutionCoordinator
|
||||
Execute[executeTool]
|
||||
SetStrategy[setStrategy]
|
||||
end
|
||||
|
||||
subgraph Strategies["ExecutionStrategy Interface"]
|
||||
Direct[DirectExecutionStrategy]
|
||||
Queued[QueuedExecutionStrategy]
|
||||
end
|
||||
|
||||
subgraph PendingCalls[PendingCallsManager]
|
||||
Wait[waitForResult]
|
||||
Resolve[resolve]
|
||||
end
|
||||
|
||||
ExecutionCoordinator --> Strategies
|
||||
Direct -->|invoke| Tool[Tool.invoke]
|
||||
Queued --> PendingCalls
|
||||
PendingCalls -.->|worker response| Resolve
|
||||
```
|
||||
|
||||
#### ExecutionStrategy Interface
|
||||
|
||||
```typescript
|
||||
interface ExecutionStrategy {
|
||||
executeTool(
|
||||
tool: Tool,
|
||||
args: Record<string, unknown>,
|
||||
context: ExecutionContext,
|
||||
): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface ExecutionContext {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### DirectExecutionStrategy
|
||||
|
||||
The default strategy that executes tools immediately in the same process:
|
||||
|
||||
```typescript
|
||||
const strategy = new DirectExecutionStrategy();
|
||||
const result = await strategy.executeTool(tool, args, context);
|
||||
// Directly calls tool.invoke(args)
|
||||
```
|
||||
|
||||
#### QueuedExecutionStrategy
|
||||
|
||||
For multi-instance deployments where tool execution happens on worker processes:
|
||||
|
||||
```typescript
|
||||
const strategy = new QueuedExecutionStrategy(
|
||||
pendingCallsManager,
|
||||
timeoutMs // Optional, defaults to 120000ms (2 minutes)
|
||||
);
|
||||
|
||||
// Methods for resolving calls from workers:
|
||||
strategy.resolveToolCall(callId, result); // Returns true if call was pending
|
||||
strategy.rejectToolCall(callId, error); // Returns true if call was pending
|
||||
strategy.getPendingCallsManager(); // Access the pending calls manager
|
||||
```
|
||||
|
||||
#### PendingCallsManager
|
||||
|
||||
Tracks tool calls waiting for results with automatic timeout handling:
|
||||
|
||||
```typescript
|
||||
const manager = new PendingCallsManager();
|
||||
|
||||
// Wait for a result (with timeout)
|
||||
const result = await manager.waitForResult(callId, toolName, args, timeoutMs);
|
||||
|
||||
// Resolve/reject from worker
|
||||
manager.resolve(callId, result);
|
||||
manager.reject(callId, error);
|
||||
|
||||
// Query and manage pending calls
|
||||
manager.has(callId); // Check if call is pending
|
||||
manager.get(callId); // Get pending call info
|
||||
manager.remove(callId); // Remove without resolving
|
||||
manager.cleanupBySessionId(sessionId); // Clean up all calls for a session
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `ExecutionStrategy.ts` - Strategy interface and ExecutionContext type
|
||||
- `DirectExecutionStrategy.ts` - Executes tools directly on main instance
|
||||
- `QueuedExecutionStrategy.ts` - Delegates to worker, waits for response (default timeout: 120s)
|
||||
- `PendingCallsManager.ts` - Tracks pending tool calls with timeout support
|
||||
- `ExecutionCoordinator.ts` - Selects and invokes strategy
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup (Normal Mode)
|
||||
|
||||
```typescript
|
||||
import { McpServer } from './McpServer';
|
||||
|
||||
const mcpServer = McpServer.instance(logger);
|
||||
|
||||
// Handle SSE setup
|
||||
await mcpServer.handleSetupRequest(req, resp, serverName, postUrl, tools);
|
||||
|
||||
// Handle POST messages
|
||||
const result = await mcpServer.handlePostMessage(req, resp, tools, serverName);
|
||||
```
|
||||
|
||||
### Queue Mode Setup
|
||||
|
||||
```typescript
|
||||
import { McpServer } from './McpServer';
|
||||
import { QueuedExecutionStrategy } from './execution';
|
||||
import { RedisSessionStore } from './RedisSessionStore';
|
||||
|
||||
const mcpServer = McpServer.instance(logger);
|
||||
|
||||
// Configure Redis session store
|
||||
mcpServer.setSessionStore(new RedisSessionStore(publisher, getKey, ttl));
|
||||
|
||||
// Configure queued execution
|
||||
mcpServer.setExecutionStrategy(
|
||||
new QueuedExecutionStrategy(mcpServer.getPendingCallsManager())
|
||||
);
|
||||
```
|
||||
|
||||
## Flow Diagrams
|
||||
|
||||
### SSE Connection Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant McpServer
|
||||
participant Transport
|
||||
participant Session
|
||||
|
||||
Client->>McpServer: GET /sse (setup)
|
||||
McpServer->>Transport: createSSE()
|
||||
Transport-->>McpServer: SSETransport
|
||||
McpServer->>Session: registerSession()
|
||||
McpServer-->>Client: SSE stream opened
|
||||
|
||||
Client->>McpServer: POST /messages (tool call)
|
||||
McpServer->>Session: getTransport()
|
||||
McpServer->>Transport: handleRequest()
|
||||
Note over McpServer: Execute tool
|
||||
Transport-->>Client: Tool result via SSE
|
||||
```
|
||||
|
||||
### Queue Mode Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Main
|
||||
participant McpServer
|
||||
participant Worker
|
||||
participant Redis
|
||||
|
||||
Client->>Main: Tool call request
|
||||
Main->>McpServer: handlePostMessage()
|
||||
McpServer->>McpServer: storePendingResponse()
|
||||
Main->>Redis: Enqueue job
|
||||
Main-->>Client: 202 Accepted
|
||||
|
||||
Redis->>Worker: Dequeue job
|
||||
Worker->>Worker: Execute tool
|
||||
Worker->>Redis: Publish result
|
||||
|
||||
Redis->>Main: mcp-response event
|
||||
Main->>McpServer: handleWorkerResponse()
|
||||
McpServer-->>Client: Result via SSE
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
McpTrigger/
|
||||
├── McpServer.ts # Main facade coordinating all subsystems
|
||||
├── McpTrigger.node.ts # n8n node implementation
|
||||
├── protocol/ # JSONRPC message parsing & formatting
|
||||
│ ├── MessageParser.ts
|
||||
│ ├── MessageFormatter.ts
|
||||
│ └── types.ts
|
||||
├── session/ # Client connection & state management
|
||||
│ ├── SessionManager.ts
|
||||
│ ├── SessionStore.ts
|
||||
│ └── InMemorySessionStore.ts
|
||||
├── transport/ # SSE & Streamable HTTP protocols
|
||||
│ ├── Transport.ts
|
||||
│ ├── SSETransport.ts
|
||||
│ ├── StreamableHttpTransport.ts
|
||||
│ └── TransportFactory.ts
|
||||
├── execution/ # Direct & queued execution strategies
|
||||
│ ├── ExecutionStrategy.ts
|
||||
│ ├── DirectExecutionStrategy.ts
|
||||
│ ├── QueuedExecutionStrategy.ts
|
||||
│ ├── PendingCallsManager.ts
|
||||
│ └── ExecutionCoordinator.ts
|
||||
└── __tests__/ # Comprehensive unit tests
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
```typescript
|
||||
// Main facade
|
||||
import { McpServer, MCP_LIST_TOOLS_REQUEST_MARKER } from './McpServer';
|
||||
import type { HandlePostResult } from './McpServer';
|
||||
|
||||
// Protocol
|
||||
import { MessageParser, MessageFormatter } from './protocol';
|
||||
import type { McpToolCallInfo, McpToolResult } from './protocol';
|
||||
|
||||
// Session
|
||||
import { InMemorySessionStore, SessionManager } from './session';
|
||||
import type { SessionStore } from './session';
|
||||
|
||||
// Transport
|
||||
import { SSETransport, StreamableHttpTransport, TransportFactory } from './transport';
|
||||
import type { McpTransport, CompressionResponse, TransportType } from './transport';
|
||||
|
||||
// Execution
|
||||
import { DirectExecutionStrategy, QueuedExecutionStrategy, PendingCallsManager, ExecutionCoordinator } from './execution';
|
||||
import type { ExecutionStrategy, ExecutionContext } from './execution';
|
||||
```
|
||||
Reference in New Issue
Block a user