converse-mcp-server 4.8.5 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,6 +98,7 @@ Once installed, you can:
98
98
 
99
99
  - **Chat with a specific model**: Ask Claude to use the chat tool with your preferred model
100
100
  - **Get consensus**: Ask Claude to use the chat tool with `mode: "consensus"` when you need multiple perspectives
101
+ - **Keep working while models think**: In Claude Code v2.1.304+, chat calls run as [MCP tasks](docs/API.md#mcp-tasks) — they move to the background immediately and the answer arrives as a task notification
101
102
  - **Run tasks in background**: Use `async: true` for long-running operations that you can check later
102
103
  - **Monitor progress**: Use the check_status tool to monitor async operations with AI-generated summaries
103
104
  - **Cancel jobs**: Use the cancel_job tool to stop running operations
package/docs/API.md CHANGED
@@ -11,36 +11,33 @@ The Converse MCP Server exposes three tools through the Model Context Protocol (
11
11
  2. **Check Status Tool** (`check_status`) — monitor and retrieve results from asynchronous jobs.
12
12
  3. **Cancel Job Tool** (`cancel_job`) — cancel a running background job.
13
13
 
14
- The chat tool runs **synchronously** (immediate response) or **asynchronously** (`async: true`, background processing polled with `check_status`). When AI summarization is enabled, the server generates titles and summaries for better context tracking.
14
+ The chat tool runs **synchronously** (immediate response), as an **MCP task** (automatic, for clients that support the [Tasks extension](#mcp-tasks)), or **asynchronously** (`async: true`, background processing polled with `check_status`). When AI summarization is enabled, the server generates titles and summaries for better context tracking.
15
15
 
16
16
  ## Transport Protocols
17
17
 
18
- The server supports two transport modes:
18
+ The server supports two transport modes. Both serve two MCP protocol eras side by side: the stateless **2026-07-28** revision (`server/discover`, per-request capabilities), which is required for [MCP tasks](#mcp-tasks), and the earlier `initialize`-based revisions for older clients.
19
19
 
20
- ### HTTP Transport (Default)
20
+ ### HTTP Transport
21
21
  - **Endpoint**: `http://localhost:3157/mcp`
22
22
  - **Protocol**: HTTP streaming with JSON-RPC 2.0
23
23
  - **Usage**: Best for development, debugging, and web integrations
24
- - **Features**: Health endpoints, CORS support, session management
24
+ - **Features**: Health endpoints, CORS support. 2026-07-28 requests are stateless; earlier-revision clients get `Mcp-Session-Id` sessions
25
25
 
26
- ### Stdio Transport (Legacy)
26
+ ### Stdio Transport (Default)
27
27
  - **Protocol**: Standard input/output with JSON-RPC 2.0
28
28
  - **Usage**: Traditional MCP client integrations
29
- - **Features**: Process-based communication, lower latency
29
+ - **Features**: Process-based communication, lower latency. The client's first message picks the protocol era for the connection
30
30
 
31
31
  **Transport Selection:**
32
32
  ```bash
33
- # Default (HTTP)
33
+ # Default (stdio)
34
34
  npm start
35
35
 
36
- # Explicit HTTP
36
+ # HTTP transport
37
37
  npm start -- --transport=http
38
38
 
39
- # Stdio transport
40
- npm start -- --transport=stdio
41
-
42
39
  # Environment variable
43
- MCP_TRANSPORT=stdio npm start
40
+ MCP_TRANSPORT=http npm start
44
41
  ```
45
42
 
46
43
  ## Chat Tool
@@ -812,6 +809,36 @@ The first request creates a continuation automatically and returns its ID; pass
812
809
  { "prompt": "What about microservices?", "continuation_id": "conv_abc123" }
813
810
  ```
814
811
 
812
+ ## MCP Tasks
813
+
814
+ The server implements the [MCP Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) (`io.modelcontextprotocol/tasks`) on protocol revision 2026-07-28 and advertises it in `server/discover`. When a `chat` call's per-request client capabilities declare the extension, the server answers the `tools/call` at once with a task handle instead of blocking until the models finish:
815
+
816
+ ```json
817
+ {
818
+ "resultType": "task",
819
+ "taskId": "task_V1StGXR8_Z5jdHi6B-myT",
820
+ "status": "working",
821
+ "statusMessage": "consensus: codex, gemini",
822
+ "createdAt": "2026-10-23T18:43:28.419Z",
823
+ "lastUpdatedAt": "2026-10-23T18:43:28.419Z",
824
+ "ttlMs": null,
825
+ "pollIntervalMs": 2000
826
+ }
827
+ ```
828
+
829
+ The client polls `tasks/get` with the `taskId`. On `completed`, the `result` field holds exactly the `CallToolResult` a blocking call would have returned, including `isError: true` for tool-level failures. `tasks/cancel` aborts the in-flight provider calls. `tasks/update` is acknowledged but never needed, because no tool asks for input mid-task.
830
+
831
+ | Behaviour | Detail |
832
+ |-----------|--------|
833
+ | Which calls become tasks | `chat` only, and only without `async: true` (that path already returns at once and is followed with `check_status`). `decide`, `check_status`, and `cancel_job` always answer directly |
834
+ | Clients without the extension | Get the normal blocking result. Earlier protocol revisions never see tasks |
835
+ | `ttlMs` | `null` while the task runs (it never expires mid-flight). Once it finishes, the task stays answerable for one hour |
836
+ | Unknown or expired `taskId` | JSON-RPC error `-32602` |
837
+ | `tasks/*` without the extension declared | JSON-RPC error `-32021` |
838
+ | Server restart | Tasks live in server memory. A restarted server answers `-32602` for earlier task IDs; completed conversations are still saved under their `continuation_id` |
839
+
840
+ **Claude Code** uses MCP tasks on v2.1.304 or later, in interactive terminal sessions and in Agent SDK or `--input-format stream-json` sessions with `CLAUDE_AUTO_BACKGROUND_TASKS=1`. A task-backed `chat` call moves to the background right away: Claude keeps working and gets the answer as a task notification, and the task appears in `/tasks` with its `statusMessage`. Set `CLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUND=1` to make Claude wait for the result instead. See [MCP tasks in Claude Code](https://code.claude.com/docs/en/mcp#mcp-tasks-for-server-authors).
841
+
815
842
  ## Asynchronous Execution
816
843
 
817
844
  Set `async: true` on a chat request for long-running work:
@@ -8,7 +8,10 @@ The Converse MCP Server follows a **functional, modular architecture** designed
8
8
 
9
9
  ```
10
10
  src/
11
- ├── index.js # Main entry point & MCP server setup
11
+ ├── index.js # Main entry point & transport startup
12
+ ├── serverFactory.js # Per-era MCP Server factory (SDK v2)
13
+ ├── router.js # Request handlers, tool dispatch, MCP tasks
14
+ ├── async/taskStore.js # MCP Tasks extension store
12
15
  ├── config.js # Configuration and environment management
13
16
  ├── systemPrompts.js # System prompts for tools
14
17
  ├── providers/ # AI provider implementations
@@ -54,55 +57,30 @@ tests/ # Test suites
54
57
 
55
58
  ### Server Setup
56
59
 
57
- **HTTP Transport (Default):**
58
- ```javascript
59
- // index.js - HTTP transport setup
60
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
61
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
62
-
63
- const server = new Server(
64
- { name: 'converse-mcp-server', version: '1.0.0' },
65
- { capabilities: { tools: {} } }
66
- );
67
-
68
- // HTTP transport on port 3157 (default)
69
- const httpTransport = new StreamableHTTPServerTransport({
70
- host: 'localhost',
71
- port: 3157
72
- });
73
- ```
60
+ The server is built on MCP TypeScript SDK v2 (`@modelcontextprotocol/server`), which serves the stateless 2026-07-28 protocol revision alongside the earlier `initialize`-based ones. The SDK builds a `Server` per connection (stdio), per request (2026-07-28 over HTTP), or per session (earlier revisions over HTTP), so nothing request-scoped lives on a `Server` instance:
74
61
 
75
- **Stdio Transport (Legacy):**
76
62
  ```javascript
77
- // Alternative stdio transport
78
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
63
+ // index.js
64
+ const router = await createRouter(config); // built once: dependencies, tools, task store
65
+ const serverFactory = createServerFactory(router, mcpConfig);
79
66
 
80
- const transport = new StdioServerTransport();
81
- await server.connect(transport);
67
+ // serverFactory({ era }) → new Server(...) + router.attach(server, { era })
68
+ // era 'modern' (2026-07-28) also advertises the Tasks extension and
69
+ // registers tasks/get, tasks/update, tasks/cancel.
70
+
71
+ serveStdio(serverFactory); // stdio: the first message picks the era
72
+ await createHTTPTransport(serverFactory, httpConfig); // HTTP: see below
82
73
  ```
83
74
 
84
75
  **Transport Selection:**
85
- - **HTTP**: Default, better for development and debugging
86
- - **Stdio**: Use `--transport=stdio` or `MCP_TRANSPORT=stdio`
76
+ - **Stdio**: Default
77
+ - **HTTP**: Use `--transport=http` or `MCP_TRANSPORT=http`. `/mcp` classifies each request with the SDK's `isLegacyRequest`. Requests carrying the 2026-07-28 `_meta` envelope go to `createMcpHandler` (stateless). Everything else keeps the `Mcp-Session-Id` session map backed by `NodeStreamableHTTPServerTransport`
87
78
 
88
79
  ### Tool Registration
89
- ```javascript
90
- // Dynamic tool registration
91
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
92
- const { name, arguments: args } = request.params;
93
-
94
- switch (name) {
95
- case 'chat':
96
- return await chatTool(args); // mode: chat | consensus | roundtable
97
- case 'check_status':
98
- return await checkStatusTool(args);
99
- case 'cancel_job':
100
- return await cancelJobTool(args);
101
- default:
102
- throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${name}`);
103
- }
104
- });
105
- ```
80
+ `router.attach(server, { era })` registers handlers by method name (`tools/call`, `tools/list`, `prompts/*`, `resources/*`) that dispatch to the tool registry in `src/tools/index.js`. Each tool is an `async (args, dependencies) => CallToolResult` function with `description` and `inputSchema` attached.
81
+
82
+ ### MCP Tasks
83
+ A tool can attach a `task` policy (`{ eligible(args), statusMessage(args) }`); `chat` does. When a `tools/call` request's per-request client capabilities declare `io.modelcontextprotocol/tasks` and the policy accepts the arguments, the router creates a task in the process-global store (`src/async/taskStore.js`) and returns the `resultType: "task"` handle immediately. The tool then runs exactly as a blocking call would, under the task's own `AbortSignal`, and its `CallToolResult` becomes the task's `result`. The store is process-global so a task ID stays answerable across HTTP requests and reconnects; finished tasks are retained for an hour.
106
84
 
107
85
  ## 🤖 Provider System
108
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "4.8.5",
3
+ "version": "4.9.0",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -93,13 +93,14 @@
93
93
  ".env.example"
94
94
  ],
95
95
  "dependencies": {
96
- "@anthropic-ai/claude-agent-sdk": "^0.3.303",
96
+ "@anthropic-ai/claude-agent-sdk": "^0.3.304",
97
97
  "@anthropic-ai/sdk": "^0.135.0",
98
- "@github/copilot-sdk": "^1.0.22",
98
+ "@github/copilot-sdk": "^1.0.23",
99
99
  "@google/genai": "^2.30.0",
100
100
  "@lydell/node-pty": "1.2.0-beta.15",
101
101
  "@mistralai/mistralai": "^2.7.0",
102
- "@modelcontextprotocol/sdk": "^1.32.1",
102
+ "@modelcontextprotocol/node": "^2.1.2",
103
+ "@modelcontextprotocol/server": "^2.4.0",
103
104
  "@openai/codex-sdk": "^0.163.0",
104
105
  "cors": "^2.8.6",
105
106
  "dotenv": "^18.0.8",
@@ -108,9 +109,11 @@
108
109
  "nanoid": "^6.0.2",
109
110
  "openai": "^7.39.0",
110
111
  "p-limit": "^7.3.3",
111
- "vite": "^8.3.4"
112
+ "vite": "^8.3.4",
113
+ "zod": "^4.6.7"
112
114
  },
113
115
  "devDependencies": {
116
+ "@modelcontextprotocol/sdk": "^1.32.1",
114
117
  "@vitest/coverage-v8": "^5.0.5",
115
118
  "cross-env": "^10.1.0",
116
119
  "eslint": "^10.13.0",
@@ -0,0 +1,239 @@
1
+ /**
2
+ * MCP Tasks extension store (io.modelcontextprotocol/tasks).
3
+ *
4
+ * A task wraps one tools/call that the server chose to answer with a handle
5
+ * instead of blocking. The wrapped call runs exactly as it would synchronously;
6
+ * its CallToolResult becomes the task's `result`, so a client polling via
7
+ * tasks/get receives the same payload a plain tools/call would have returned.
8
+ *
9
+ * The store is process-global rather than per-connection: clients poll on new
10
+ * connections (HTTP reconnects, resumed sessions), so a task ID must stay
11
+ * answerable for as long as this process holds the record.
12
+ */
13
+
14
+ import { LRUCache } from 'lru-cache';
15
+ import { nanoid } from 'nanoid';
16
+ import { createLogger } from '../utils/logger.js';
17
+
18
+ const logger = createLogger('task-store');
19
+
20
+ export const TASKS_EXTENSION_ID = 'io.modelcontextprotocol/tasks';
21
+
22
+ export const TASK_STATUS = {
23
+ WORKING: 'working',
24
+ INPUT_REQUIRED: 'input_required',
25
+ COMPLETED: 'completed',
26
+ FAILED: 'failed',
27
+ CANCELLED: 'cancelled',
28
+ };
29
+
30
+ const TERMINAL_STATUSES = new Set([
31
+ TASK_STATUS.COMPLETED,
32
+ TASK_STATUS.FAILED,
33
+ TASK_STATUS.CANCELLED,
34
+ ]);
35
+
36
+ // Model calls take seconds to minutes; polling faster than this only adds load.
37
+ export const DEFAULT_POLL_INTERVAL_MS = 2000;
38
+
39
+ // How long a task stays answerable after it reaches a terminal status. Clients
40
+ // poll every few seconds, so this only needs to cover a client that was away
41
+ // (disconnected, resumed later) when the task finished.
42
+ export const DEFAULT_TERMINAL_RETENTION_MS = 60 * 60 * 1000;
43
+
44
+ // Bounds memory if a client creates tasks and never polls them.
45
+ const MAX_TASKS = 1000;
46
+
47
+ const JSONRPC_INTERNAL_ERROR = -32603;
48
+
49
+ /**
50
+ * True when the request's per-request client capabilities declare the tasks
51
+ * extension. Servers must never answer with a task otherwise.
52
+ * @param {object|undefined} meta - The request's `params._meta`
53
+ */
54
+ export function clientSupportsTasks(meta) {
55
+ const extensions =
56
+ meta?.['io.modelcontextprotocol/clientCapabilities']?.extensions;
57
+ return Boolean(
58
+ extensions &&
59
+ typeof extensions === 'object' &&
60
+ Object.hasOwn(extensions, TASKS_EXTENSION_ID),
61
+ );
62
+ }
63
+
64
+ export function isTerminalStatus(status) {
65
+ return TERMINAL_STATUSES.has(status);
66
+ }
67
+
68
+ export class TaskStore {
69
+ /**
70
+ * @param {object} [options]
71
+ * @param {number} [options.terminalRetentionMs]
72
+ * @param {number} [options.pollIntervalMs]
73
+ */
74
+ constructor(options = {}) {
75
+ this.terminalRetentionMs =
76
+ options.terminalRetentionMs ?? DEFAULT_TERMINAL_RETENTION_MS;
77
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
78
+ // Running tasks have no TTL (ttl: 0 on set); a task only starts expiring
79
+ // once it is terminal, so a long model call is never evicted mid-flight.
80
+ this.tasks = new LRUCache({
81
+ max: MAX_TASKS,
82
+ ttlAutopurge: false,
83
+ allowStale: false,
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Create a task and start `run` in the background. Resolves once the task
89
+ * record exists, before the work finishes, so the caller can answer the
90
+ * tools/call with the handle immediately.
91
+ * @param {object} params
92
+ * @param {string} params.toolName
93
+ * @param {string} [params.statusMessage]
94
+ * @param {(signal: AbortSignal) => Promise<object>} params.run - Produces the CallToolResult
95
+ * @returns {object} The CreateTaskResult wire object
96
+ */
97
+ create({ toolName, statusMessage, run }) {
98
+ const now = new Date().toISOString();
99
+ const record = {
100
+ taskId: `task_${nanoid(21)}`,
101
+ toolName,
102
+ status: TASK_STATUS.WORKING,
103
+ statusMessage,
104
+ createdAt: now,
105
+ lastUpdatedAt: now,
106
+ controller: new AbortController(),
107
+ result: undefined,
108
+ error: undefined,
109
+ };
110
+ this.tasks.set(record.taskId, record, { ttl: 0 });
111
+
112
+ logger.info('Task created', {
113
+ data: { taskId: record.taskId, tool: toolName },
114
+ });
115
+
116
+ // Detached on purpose: the tools/call response must not wait for the work.
117
+ Promise.resolve()
118
+ .then(() => run(record.controller.signal))
119
+ .then(
120
+ (result) => this.#settle(record, TASK_STATUS.COMPLETED, { result }),
121
+ (error) =>
122
+ this.#settle(record, TASK_STATUS.FAILED, {
123
+ error: {
124
+ code: JSONRPC_INTERNAL_ERROR,
125
+ message: error?.message || 'Task failed',
126
+ },
127
+ }),
128
+ );
129
+
130
+ return { resultType: 'task', ...this.#toWire(record) };
131
+ }
132
+
133
+ /**
134
+ * The current task state for tasks/get, or undefined for an unknown ID.
135
+ */
136
+ get(taskId) {
137
+ const record = this.tasks.get(taskId);
138
+ if (!record) return undefined;
139
+
140
+ const wire = { resultType: 'complete', ...this.#toWire(record) };
141
+ if (record.status === TASK_STATUS.COMPLETED) {
142
+ wire.result = record.result;
143
+ } else if (record.status === TASK_STATUS.FAILED) {
144
+ wire.error = record.error;
145
+ }
146
+ return wire;
147
+ }
148
+
149
+ /**
150
+ * Cooperative cancellation for tasks/cancel. Returns false for an unknown ID.
151
+ * A task that is already terminal keeps its status.
152
+ */
153
+ cancel(taskId) {
154
+ const record = this.tasks.get(taskId);
155
+ if (!record) return false;
156
+ if (isTerminalStatus(record.status)) return true;
157
+
158
+ record.controller.abort(new Error('Task cancelled by client'));
159
+ this.#settle(record, TASK_STATUS.CANCELLED, {
160
+ statusMessage: 'Cancelled by client',
161
+ });
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Abort every running task. Used on shutdown so in-flight provider calls stop.
167
+ */
168
+ abortAll(reason = 'Server shutting down') {
169
+ for (const record of this.tasks.values()) {
170
+ if (!isTerminalStatus(record.status)) {
171
+ record.controller.abort(new Error(reason));
172
+ }
173
+ }
174
+ }
175
+
176
+ get size() {
177
+ return this.tasks.size;
178
+ }
179
+
180
+ #settle(record, status, { result, error, statusMessage } = {}) {
181
+ // The first terminal transition wins: a cancelled task whose work later
182
+ // resolves (or rejects on the abort) stays cancelled.
183
+ if (isTerminalStatus(record.status)) return;
184
+
185
+ record.status = status;
186
+ record.result = result;
187
+ record.error = error;
188
+ record.statusMessage = statusMessage;
189
+ record.lastUpdatedAt = new Date().toISOString();
190
+ this.tasks.set(record.taskId, record, { ttl: this.terminalRetentionMs });
191
+
192
+ logger.info('Task settled', {
193
+ data: {
194
+ taskId: record.taskId,
195
+ tool: record.toolName,
196
+ status,
197
+ isError: result?.isError === true,
198
+ },
199
+ });
200
+ }
201
+
202
+ #toWire(record) {
203
+ const wire = {
204
+ taskId: record.taskId,
205
+ status: record.status,
206
+ createdAt: record.createdAt,
207
+ lastUpdatedAt: record.lastUpdatedAt,
208
+ ttlMs: this.#ttlMs(record),
209
+ pollIntervalMs: this.pollIntervalMs,
210
+ };
211
+ if (record.statusMessage) {
212
+ wire.statusMessage = record.statusMessage;
213
+ }
214
+ return wire;
215
+ }
216
+
217
+ // The spec measures ttlMs from createdAt. A running task never expires here,
218
+ // so it reports null; a terminal one is kept for the retention window past
219
+ // the moment it settled.
220
+ #ttlMs(record) {
221
+ if (!isTerminalStatus(record.status)) return null;
222
+ const settledAfterMs =
223
+ Date.parse(record.lastUpdatedAt) - Date.parse(record.createdAt);
224
+ return settledAfterMs + this.terminalRetentionMs;
225
+ }
226
+ }
227
+
228
+ let taskStore = null;
229
+
230
+ export function getTaskStore() {
231
+ if (!taskStore) {
232
+ taskStore = new TaskStore();
233
+ }
234
+ return taskStore;
235
+ }
236
+
237
+ export function setTaskStore(store) {
238
+ taskStore = store;
239
+ }
package/src/index.js CHANGED
@@ -8,8 +8,7 @@
8
8
  */
9
9
 
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
13
12
  import {
14
13
  loadConfig,
15
14
  validateRuntimeConfig,
@@ -17,6 +16,7 @@ import {
17
16
  getHttpTransportConfig,
18
17
  } from './config.js';
19
18
  import { createRouter } from './router.js';
19
+ import { createServerFactory } from './serverFactory.js';
20
20
  import { createHTTPTransport } from './transport/httpTransport.js';
21
21
  import { createLogger, startTimer } from './utils/logger.js';
22
22
  import { debugError, forceLog } from './utils/console.js';
@@ -135,27 +135,22 @@ async function main() {
135
135
 
136
136
  logger.info('Using transport type', { data: { transport: transportType } });
137
137
 
138
- logger.debug('Creating MCP server instance', {
138
+ logger.debug('Creating MCP server factory', {
139
139
  data: { name: mcpConfig.name, version: mcpConfig.version },
140
140
  });
141
141
 
142
- // Create MCP server with configuration
143
- const server = new Server(
144
- {
145
- name: mcpConfig.name,
146
- version: mcpConfig.version,
147
- },
148
- mcpConfig,
149
- );
150
-
151
- // Set up router with server and config
152
- await createRouter(server, config);
142
+ const router = await createRouter(config);
143
+ const serverFactory = createServerFactory(router, mcpConfig);
153
144
 
154
145
  // Start server with appropriate transport
155
146
  if (transportType === 'http') {
156
147
  // HTTP streaming transport with full configuration
157
148
  const httpConfig = getHttpTransportConfig(config);
158
- const httpTransport = await createHTTPTransport(server, httpConfig);
149
+ const httpTransport = await createHTTPTransport(serverFactory, {
150
+ ...httpConfig,
151
+ serverName: mcpConfig.name,
152
+ serverVersion: mcpConfig.version,
153
+ });
159
154
 
160
155
  await httpTransport.start();
161
156
  const status = httpTransport.getStatus();
@@ -176,9 +171,12 @@ async function main() {
176
171
  // Store reference for shutdown
177
172
  process.httpTransport = httpTransport;
178
173
  } else {
179
- // Stdio transport (legacy)
180
- const transport = new StdioServerTransport();
181
- await server.connect(transport);
174
+ // The opening message decides the connection's era: server/discover (or
175
+ // an envelope-carrying request) pins a 2026-07-28 instance, initialize
176
+ // pins a legacy one.
177
+ serveStdio(serverFactory, {
178
+ onerror: (error) => logger.error('Stdio transport error', { error }),
179
+ });
182
180
 
183
181
  const startupTime = serverTimer('completed');
184
182
  logger.info(
@@ -227,6 +225,9 @@ async function gracefulShutdown(signal) {
227
225
  stopAsyncJobStoreCleanup();
228
226
  stopFileCacheCleanup();
229
227
  logger.info('Cleanup timers stopped successfully');
228
+
229
+ const { getTaskStore } = await import('./async/taskStore.js');
230
+ getTaskStore().abortAll();
230
231
  } catch (error) {
231
232
  logger.error('Error stopping cleanup timers', { error });
232
233
  }
package/src/router.js CHANGED
@@ -7,13 +7,11 @@
7
7
  */
8
8
 
9
9
  import {
10
- CallToolRequestSchema,
11
- ListToolsRequestSchema,
12
- ListPromptsRequestSchema,
13
- GetPromptRequestSchema,
14
- ListResourcesRequestSchema,
15
- ReadResourceRequestSchema,
16
- } from '@modelcontextprotocol/sdk/types.js';
10
+ MissingRequiredClientCapabilityError,
11
+ ProtocolError,
12
+ ProtocolErrorCode,
13
+ } from '@modelcontextprotocol/server';
14
+ import { z } from 'zod';
17
15
  import { getContinuationStore } from './continuationStore.js';
18
16
  import { getTools } from './tools/index.js';
19
17
  import { getProviders } from './providers/index.js';
@@ -27,6 +25,11 @@ import { processUnifiedContext } from './utils/contextProcessor.js';
27
25
  import { getAsyncJobStore } from './async/asyncJobStore.js';
28
26
  import { getJobRunner } from './async/jobRunner.js';
29
27
  import { getFileCache } from './async/fileCache.js';
28
+ import {
29
+ getTaskStore,
30
+ clientSupportsTasks,
31
+ TASKS_EXTENSION_ID,
32
+ } from './async/taskStore.js';
30
33
  import providerStreamNormalizer from './async/providerStreamNormalizer.js';
31
34
  import { createLogger, startTimer } from './utils/logger.js';
32
35
  import { debugError } from './utils/console.js';
@@ -177,12 +180,63 @@ async function createDependencies(config, context = {}) {
177
180
  }
178
181
 
179
182
  /**
180
- * Creates and configures the central router for handling MCP requests
181
- * @param {object} server - MCP Server instance
183
+ * Resolve a tool and validate its arguments, throwing on either failure.
184
+ * @returns {Function} The tool handler
185
+ */
186
+ function prepareTool(toolName, toolArgs, tools) {
187
+ const { tool } = validateTool(toolName, tools);
188
+
189
+ if (tool.inputSchema) {
190
+ const isValidArgs = validateToolArguments(toolArgs, tool.inputSchema);
191
+ if (!isValidArgs) {
192
+ throw new ValidationError(
193
+ `Invalid arguments for tool ${toolName}`,
194
+ ERROR_CODES.INVALID_TOOL_ARGS,
195
+ {
196
+ providedArgs: Object.keys(toolArgs),
197
+ expectedSchema: tool.inputSchema,
198
+ },
199
+ );
200
+ }
201
+ }
202
+
203
+ return tool;
204
+ }
205
+
206
+ const TaskIdParamsSchema = z.object({ taskId: z.string() });
207
+
208
+ // inputResponses is lifted out of params by the SDK before validation (it is
209
+ // read from ctx.mcpReq.inputResponses), so it must stay optional here.
210
+ const TaskUpdateParamsSchema = z.object({
211
+ taskId: z.string(),
212
+ inputResponses: z.record(z.string(), z.unknown()).optional(),
213
+ });
214
+
215
+ function requireTasksCapability(ctx) {
216
+ if (!clientSupportsTasks(ctx?.mcpReq?.envelope)) {
217
+ throw new MissingRequiredClientCapabilityError({
218
+ requiredCapabilities: { extensions: { [TASKS_EXTENSION_ID]: {} } },
219
+ });
220
+ }
221
+ }
222
+
223
+ function taskNotFoundError(taskId) {
224
+ return new ProtocolError(
225
+ ProtocolErrorCode.InvalidParams,
226
+ 'Failed to retrieve task: Task not found',
227
+ { taskId },
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Creates the central router. Built once per process; `attach` registers its
233
+ * handlers on every Server instance the transports create (one per stdio
234
+ * connection, per modern HTTP request, and per legacy HTTP session), so all of
235
+ * them share the same dependencies and the same task store.
182
236
  * @param {object} config - Configuration object with provider settings
183
- * @returns {Promise<void>}
237
+ * @returns {Promise<object>} Router interface: attach, listTools, callTool
184
238
  */
185
- export async function createRouter(server, config) {
239
+ export async function createRouter(config) {
186
240
  const createRouterLogger = logger.operation('createRouter');
187
241
  const timer = startTimer('router-initialization', 'router');
188
242
 
@@ -192,16 +246,19 @@ export async function createRouter(server, config) {
192
246
  // Initialize dependencies with validation
193
247
  const dependencies = await createDependencies(config);
194
248
  const tools = getTools(config);
249
+ const taskStore = getTaskStore();
195
250
 
196
251
  createRouterLogger.info(
197
252
  `Router initialized with ${Object.keys(tools).length} tools`,
198
253
  );
199
254
 
200
- // Register unified tool call handler with enhanced error handling
201
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
255
+ /**
256
+ * Run a tool to completion. Always resolves to a CallToolResult: failures
257
+ * become `isError` results, which is what both a plain tools/call and a
258
+ * completed task carry.
259
+ */
260
+ const executeTool = async (toolName, toolArgs, { signal, requestId }) => {
202
261
  const toolTimer = startTimer('tool-execution', 'router');
203
- const toolName = request.params?.name;
204
- const toolArgs = request.params?.arguments || {};
205
262
  const requestLogger = logger.operation(`tool-call:${toolName}`);
206
263
 
207
264
  try {
@@ -209,33 +266,14 @@ export async function createRouter(server, config) {
209
266
  data: { toolName, argCount: Object.keys(toolArgs).length },
210
267
  });
211
268
 
212
- // Validate tool existence and callability
213
- const { tool } = validateTool(toolName, tools);
214
-
215
- // Validate tool arguments if schema is provided
216
- if (tool.inputSchema) {
217
- const isValidArgs = validateToolArguments(toolArgs, tool.inputSchema);
218
- if (!isValidArgs) {
219
- throw new ValidationError(
220
- `Invalid arguments for tool ${toolName}`,
221
- ERROR_CODES.INVALID_TOOL_ARGS,
222
- {
223
- providedArgs: Object.keys(toolArgs),
224
- expectedSchema: tool.inputSchema,
225
- },
226
- );
227
- }
228
- }
229
-
230
- // Execute the tool with dependency injection (signal is per-request from MCP SDK)
231
- const result = await tool(toolArgs, { ...dependencies, signal: extra?.signal });
269
+ const tool = prepareTool(toolName, toolArgs, tools);
270
+ const result = await tool(toolArgs, { ...dependencies, signal });
232
271
 
233
272
  const executionTime = toolTimer('completed');
234
273
  requestLogger.info('Tool execution completed', {
235
274
  data: { executionTime: `${executionTime}ms` },
236
275
  });
237
276
 
238
- // Ensure result has proper format
239
277
  if (!result || !result.content) {
240
278
  throw new ToolError(
241
279
  `Tool ${toolName} returned invalid result format`,
@@ -250,7 +288,7 @@ export async function createRouter(server, config) {
250
288
  const executionTime = toolTimer('failed');
251
289
 
252
290
  // Detect cancellation — log at debug, not error
253
- if (extra?.signal?.aborted || error.name === 'AbortError') {
291
+ if (signal?.aborted || error.name === 'AbortError') {
254
292
  requestLogger.debug('Tool execution cancelled by client', {
255
293
  data: { executionTime: `${executionTime}ms` },
256
294
  });
@@ -268,13 +306,54 @@ export async function createRouter(server, config) {
268
306
  return createErrorResponse(error, toolName, {
269
307
  executionTime,
270
308
  arguments: Object.keys(toolArgs),
271
- requestId: request.id || 'unknown',
309
+ requestId,
272
310
  });
273
311
  }
274
- });
312
+ };
275
313
 
276
- // Register enhanced list_tools handler
277
- server.setRequestHandler(ListToolsRequestSchema, async () => {
314
+ /**
315
+ * tools/call: answer with a task handle when the tool is long-running and
316
+ * this request's client declared the tasks extension; otherwise block.
317
+ */
318
+ const handleToolCall = async (request, ctx) => {
319
+ const toolName = request.params?.name;
320
+ const toolArgs = request.params?.arguments || {};
321
+ const requestId = ctx?.mcpReq?.id ?? 'unknown';
322
+ const taskPolicy = tools[toolName]?.task;
323
+
324
+ if (
325
+ taskPolicy &&
326
+ clientSupportsTasks(ctx?.mcpReq?.envelope) &&
327
+ taskPolicy.eligible(toolArgs)
328
+ ) {
329
+ // Bad arguments are reported on the call itself rather than as a task
330
+ // that fails on its first poll.
331
+ try {
332
+ prepareTool(toolName, toolArgs, tools);
333
+ } catch (error) {
334
+ return createErrorResponse(error, toolName, {
335
+ arguments: Object.keys(toolArgs),
336
+ requestId,
337
+ });
338
+ }
339
+
340
+ // The task owns its own abort signal: this request finishes as soon as
341
+ // the handle is returned, and only tasks/cancel may stop the work.
342
+ return taskStore.create({
343
+ toolName,
344
+ statusMessage: taskPolicy.statusMessage?.(toolArgs),
345
+ run: (taskSignal) =>
346
+ executeTool(toolName, toolArgs, { signal: taskSignal, requestId }),
347
+ });
348
+ }
349
+
350
+ return executeTool(toolName, toolArgs, {
351
+ signal: ctx?.mcpReq?.signal,
352
+ requestId,
353
+ });
354
+ };
355
+
356
+ const handleListTools = async () => {
278
357
  try {
279
358
  const toolList = Object.entries(tools).map(([name, handler]) => {
280
359
  const toolInfo = {
@@ -315,17 +394,9 @@ export async function createRouter(server, config) {
315
394
  { error: error.message },
316
395
  );
317
396
  }
318
- });
319
-
320
- // Register list_prompts handler
321
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
322
- return {
323
- prompts: [helpPromptMetadata],
324
- };
325
- });
397
+ };
326
398
 
327
- // Register get_prompt handler
328
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
399
+ const handleGetPrompt = async (request) => {
329
400
  const promptName = request.params?.name;
330
401
 
331
402
  if (promptName === 'help') {
@@ -343,18 +414,9 @@ export async function createRouter(server, config) {
343
414
  'PROMPT_NOT_FOUND',
344
415
  { requestedPrompt: promptName },
345
416
  );
346
- });
347
-
348
- // Register list_resources handler
349
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
350
- const resources = listResources();
351
- return {
352
- resources,
353
- };
354
- });
417
+ };
355
418
 
356
- // Register read_resource handler
357
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
419
+ const handleReadResource = async (request) => {
358
420
  const resourceUri = request.params?.uri;
359
421
 
360
422
  if (resourceUri === helpResourceMetadata.uri) {
@@ -366,9 +428,73 @@ export async function createRouter(server, config) {
366
428
  'RESOURCE_NOT_FOUND',
367
429
  { requestedResource: resourceUri },
368
430
  );
369
- });
431
+ };
370
432
 
371
- // Note: Custom health endpoint removed - MCP uses standard protocol methods only
433
+ const handleGetTask = async ({ taskId }, ctx) => {
434
+ requireTasksCapability(ctx);
435
+ const task = taskStore.get(taskId);
436
+ if (!task) {
437
+ throw taskNotFoundError(taskId);
438
+ }
439
+ return task;
440
+ };
441
+
442
+ // No tool asks for input mid-task, so there is never an outstanding
443
+ // inputRequest to fulfil; the spec says to ignore unknown keys and ack.
444
+ const handleUpdateTask = async ({ taskId }, ctx) => {
445
+ requireTasksCapability(ctx);
446
+ if (!taskStore.get(taskId)) {
447
+ throw taskNotFoundError(taskId);
448
+ }
449
+ return {};
450
+ };
451
+
452
+ const handleCancelTask = async ({ taskId }, ctx) => {
453
+ requireTasksCapability(ctx);
454
+ if (!taskStore.cancel(taskId)) {
455
+ throw taskNotFoundError(taskId);
456
+ }
457
+ return {};
458
+ };
459
+
460
+ /**
461
+ * Register the router's handlers on one Server instance.
462
+ * @param {object} server - MCP Server instance
463
+ * @param {object} [options]
464
+ * @param {'legacy'|'modern'} [options.era] - Protocol era the instance serves.
465
+ * The tasks/* methods exist only on the modern (2026-07-28) era; on the
466
+ * legacy era those names belong to the removed 2025-11-25 tasks design.
467
+ */
468
+ const attach = (server, { era = 'legacy' } = {}) => {
469
+ server.setRequestHandler('tools/call', handleToolCall);
470
+ server.setRequestHandler('tools/list', handleListTools);
471
+ server.setRequestHandler('prompts/list', async () => ({
472
+ prompts: [helpPromptMetadata],
473
+ }));
474
+ server.setRequestHandler('prompts/get', handleGetPrompt);
475
+ server.setRequestHandler('resources/list', async () => ({
476
+ resources: listResources(),
477
+ }));
478
+ server.setRequestHandler('resources/read', handleReadResource);
479
+
480
+ if (era === 'modern') {
481
+ server.setRequestHandler(
482
+ 'tasks/get',
483
+ { params: TaskIdParamsSchema },
484
+ handleGetTask,
485
+ );
486
+ server.setRequestHandler(
487
+ 'tasks/update',
488
+ { params: TaskUpdateParamsSchema },
489
+ handleUpdateTask,
490
+ );
491
+ server.setRequestHandler(
492
+ 'tasks/cancel',
493
+ { params: TaskIdParamsSchema },
494
+ handleCancelTask,
495
+ );
496
+ }
497
+ };
372
498
 
373
499
  timer('completed');
374
500
  createRouterLogger.info('Router configured successfully', {
@@ -380,8 +506,9 @@ export async function createRouter(server, config) {
380
506
  },
381
507
  });
382
508
 
383
- // Return router interface for testing purposes
384
509
  return {
510
+ attach,
511
+
385
512
  listTools: async () => {
386
513
  const tools = getTools(config);
387
514
  return {
@@ -405,28 +532,7 @@ export async function createRouter(server, config) {
405
532
  const toolArgs = toolCall.arguments || {};
406
533
 
407
534
  try {
408
- // Validate tool existence and callability
409
- const { tool } = validateTool(toolName, tools);
410
-
411
- // Validate tool arguments if schema is provided
412
- if (tool.inputSchema) {
413
- const isValidArgs = validateToolArguments(
414
- toolArgs,
415
- tool.inputSchema,
416
- );
417
- if (!isValidArgs) {
418
- throw new ValidationError(
419
- `Invalid arguments for tool ${toolName}`,
420
- ERROR_CODES.INVALID_TOOL_ARGS,
421
- {
422
- providedArgs: Object.keys(toolArgs),
423
- expectedSchema: tool.inputSchema,
424
- },
425
- );
426
- }
427
- }
428
-
429
- // Execute the tool with dependency injection (signal is per-request)
535
+ const tool = prepareTool(toolName, toolArgs, tools);
430
536
  return await tool(toolArgs, { ...dependencies, signal });
431
537
  } catch (error) {
432
538
  return createErrorResponse(error, toolName, {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * MCP Server factory
3
+ *
4
+ * The SDK builds a fresh Server per stdio connection, per modern (2026-07-28)
5
+ * HTTP request, and per legacy HTTP session, and tells the factory which
6
+ * protocol era the instance will serve. All instances share one router, so
7
+ * dependencies and the task store live outside any single Server.
8
+ */
9
+
10
+ import { Server } from '@modelcontextprotocol/server';
11
+ import { TASKS_EXTENSION_ID } from './async/taskStore.js';
12
+
13
+ /**
14
+ * @param {object} router - Router from createRouter()
15
+ * @param {object} mcpConfig - Result of getMcpClientConfig()
16
+ * @returns {(ctx: { era: 'legacy'|'modern' }) => Server}
17
+ */
18
+ export function createServerFactory(router, mcpConfig) {
19
+ return ({ era } = {}) => {
20
+ const capabilities = { ...mcpConfig.capabilities };
21
+ // Tasks are only served on the modern era, where tasks/get, tasks/update
22
+ // and tasks/cancel are registered; advertising them to a legacy client
23
+ // would point it at the removed 2025-11-25 task methods.
24
+ if (era === 'modern') {
25
+ capabilities.extensions = { [TASKS_EXTENSION_ID]: {} };
26
+ }
27
+
28
+ const server = new Server(
29
+ { name: mcpConfig.name, version: mcpConfig.version },
30
+ { capabilities },
31
+ );
32
+ router.attach(server, { era });
33
+ return server;
34
+ };
35
+ }
package/src/tools/chat.js CHANGED
@@ -1209,3 +1209,15 @@ chatTool.inputSchema = {
1209
1209
  },
1210
1210
  required: ['prompt'],
1211
1211
  };
1212
+
1213
+ // Every model call takes seconds to minutes, so a client that supports the MCP
1214
+ // Tasks extension gets a task handle instead of a blocked call. `async: true`
1215
+ // is excluded: it already returns at once and is followed via check_status.
1216
+ chatTool.task = {
1217
+ eligible: (args) => args.async !== true,
1218
+ statusMessage: (args) => {
1219
+ const mode = args.mode ?? 'chat';
1220
+ const models = Array.isArray(args.models) ? args.models : ['auto'];
1221
+ return `${mode}: ${models.join(', ')}`;
1222
+ },
1223
+ };
@@ -60,6 +60,7 @@ export function getTools(config = null) {
60
60
  };
61
61
  wrappedTool.description = tool.description;
62
62
  wrappedTool.inputSchema = modifiedSchema;
63
+ wrappedTool.task = tool.task;
63
64
 
64
65
  filteredTools[name] = wrappedTool;
65
66
  } else {
@@ -1,15 +1,27 @@
1
1
  /**
2
2
  * HTTP Streaming Transport for MCP Server
3
3
  *
4
- * Implements StreamableHTTPServerTransport to replace stdio transport,
5
- * eliminating console output interference and providing better local development experience.
4
+ * Serves both protocol eras on one /mcp endpoint. Requests carrying the
5
+ * 2026-07-28 per-request envelope are stateless and go to the SDK's modern
6
+ * handler, which builds a server per request. Everything else is 2025-era
7
+ * traffic and keeps its session model: initialize creates a sessionful
8
+ * transport, and later POST/GET/DELETE requests find it by Mcp-Session-Id.
6
9
  */
7
10
 
8
11
  import express from 'express';
9
12
  import cors from 'cors';
10
13
  import { randomUUID } from 'node:crypto';
11
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
14
+ import {
15
+ createMcpHandler,
16
+ isInitializeRequest,
17
+ isLegacyRequest,
18
+ } from '@modelcontextprotocol/server';
19
+ import {
20
+ NodeStreamableHTTPServerTransport,
21
+ hostHeaderValidation,
22
+ toNodeHandler,
23
+ toWebRequest,
24
+ } from '@modelcontextprotocol/node';
13
25
  import { createLogger } from '../utils/logger.js';
14
26
  import {
15
27
  registerTransportSession,
@@ -41,7 +53,14 @@ export class HTTPTransportServer {
41
53
  corsOptions: config.corsOptions || {
42
54
  origin: '*',
43
55
  methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
44
- allowedHeaders: ['Content-Type', 'mcp-session-id', 'Authorization'],
56
+ allowedHeaders: [
57
+ 'Content-Type',
58
+ 'Authorization',
59
+ 'mcp-session-id',
60
+ 'mcp-protocol-version',
61
+ 'mcp-method',
62
+ 'mcp-name',
63
+ ],
45
64
  credentials: false,
46
65
  exposedHeaders: ['Mcp-Session-Id'],
47
66
  },
@@ -61,17 +80,27 @@ export class HTTPTransportServer {
61
80
  this.server = null;
62
81
  this.transports = new Map(); // sessionId -> transport
63
82
  this.sessionTimers = new Map(); // sessionId -> timeout timer
64
- this.mcpServer = null;
83
+ this.serverFactory = null;
84
+ this.modernHandler = null;
65
85
  this.isStarted = false;
66
86
  this.cleanupInterval = null;
67
87
  }
68
88
 
69
89
  /**
70
90
  * Initialize the HTTP transport server
71
- * @param {object} mcpServer - MCP Server instance
91
+ * @param {Function} serverFactory - Builds an MCP Server for a given era
72
92
  */
73
- async initialize(mcpServer) {
74
- this.mcpServer = mcpServer;
93
+ async initialize(serverFactory) {
94
+ this.serverFactory = serverFactory;
95
+ // Legacy traffic never reaches this handler (it is routed to the session
96
+ // map first), so 'reject' only guards against a misrouted request.
97
+ this.modernHandler = toNodeHandler(
98
+ createMcpHandler(serverFactory, {
99
+ legacy: 'reject',
100
+ onerror: (error) =>
101
+ logger.error('Modern MCP request failed', { error }),
102
+ }),
103
+ );
75
104
  this.setupMiddleware();
76
105
  this.setupRoutes();
77
106
  return this;
@@ -160,6 +189,14 @@ export class HTTPTransportServer {
160
189
  });
161
190
  }
162
191
 
192
+ // Applied ahead of era routing so both eras get the same protection.
193
+ if (this.config.enableDnsRebindingProtection) {
194
+ const validateHost = hostHeaderValidation(this.config.allowedHosts);
195
+ this.app.use('/mcp', (req, res, next) => {
196
+ if (validateHost(req, res)) next();
197
+ });
198
+ }
199
+
163
200
  // CORS configuration for browser clients
164
201
  if (this.config.enableCors) {
165
202
  this.app.use(cors(this.config.corsOptions));
@@ -185,19 +222,34 @@ export class HTTPTransportServer {
185
222
  * Setup MCP HTTP routes
186
223
  */
187
224
  setupRoutes() {
188
- // Main MCP endpoint - handles POST requests for client-to-server communication
189
- this.app.post('/mcp', async (req, res) => {
190
- await this.handleMcpRequest(req, res);
191
- });
192
-
193
- // SSE endpoint - handles GET requests for server-to-client notifications
194
- this.app.get('/mcp', async (req, res) => {
195
- await this.handleSseRequest(req, res);
196
- });
225
+ this.app.all('/mcp', async (req, res) => {
226
+ try {
227
+ const webRequest = await toWebRequest(req, req.body);
228
+ if (!(await isLegacyRequest(webRequest, req.body))) {
229
+ await this.modernHandler(req, res, req.body);
230
+ return;
231
+ }
232
+ } catch (error) {
233
+ logger.error('Error classifying MCP request', { error });
234
+ if (!res.headersSent) {
235
+ res.status(500).json({
236
+ jsonrpc: '2.0',
237
+ error: { code: -32603, message: 'Internal server error' },
238
+ id: null,
239
+ });
240
+ }
241
+ return;
242
+ }
197
243
 
198
- // Session termination - handles DELETE requests
199
- this.app.delete('/mcp', async (req, res) => {
200
- await this.handleSessionTermination(req, res);
244
+ if (req.method === 'POST') {
245
+ await this.handleMcpRequest(req, res);
246
+ } else if (req.method === 'GET') {
247
+ await this.handleSseRequest(req, res);
248
+ } else if (req.method === 'DELETE') {
249
+ await this.handleSessionTermination(req, res);
250
+ } else {
251
+ res.status(405).set('Allow', 'GET, POST, DELETE').end();
252
+ }
201
253
  });
202
254
 
203
255
  // Health check endpoint
@@ -205,7 +257,7 @@ export class HTTPTransportServer {
205
257
  res.json({
206
258
  status: 'healthy',
207
259
  transport: 'http',
208
- server: this.mcpServer ? 'connected' : 'disconnected',
260
+ server: this.serverFactory ? 'connected' : 'disconnected',
209
261
  sessions: this.transports.size,
210
262
  timestamp: new Date().toISOString(),
211
263
  });
@@ -214,8 +266,8 @@ export class HTTPTransportServer {
214
266
  // Server info endpoint
215
267
  this.app.get('/info', (req, res) => {
216
268
  res.json({
217
- name: this.mcpServer?.serverCapabilities?.name || 'unknown',
218
- version: this.mcpServer?.serverCapabilities?.version || 'unknown',
269
+ name: this.config.serverName || 'unknown',
270
+ version: this.config.serverVersion || 'unknown',
219
271
  transport: 'http-streaming',
220
272
  endpoints: {
221
273
  mcp: '/mcp',
@@ -363,7 +415,7 @@ export class HTTPTransportServer {
363
415
  * Create a new MCP transport with session management
364
416
  */
365
417
  async createNewTransport() {
366
- const transport = new StreamableHTTPServerTransport({
418
+ const transport = new NodeStreamableHTTPServerTransport({
367
419
  sessionIdGenerator: () => randomUUID(),
368
420
  onsessioninitialized: (sessionId) => {
369
421
  this.transports.set(sessionId, transport);
@@ -372,8 +424,6 @@ export class HTTPTransportServer {
372
424
  registerTransportSession(transport, sessionId);
373
425
  logger.debug('Transport session initialized', { data: { sessionId } });
374
426
  },
375
- enableDnsRebindingProtection: this.config.enableDnsRebindingProtection,
376
- allowedHosts: this.config.allowedHosts,
377
427
  });
378
428
 
379
429
  // Clean up transport when closed
@@ -387,8 +437,7 @@ export class HTTPTransportServer {
387
437
  }
388
438
  };
389
439
 
390
- // Connect to the MCP server
391
- await this.mcpServer.connect(transport);
440
+ await this.serverFactory({ era: 'legacy' }).connect(transport);
392
441
 
393
442
  return transport;
394
443
  }
@@ -573,12 +622,12 @@ export class HTTPTransportServer {
573
622
 
574
623
  /**
575
624
  * Factory function to create and configure HTTP transport server
576
- * @param {object} mcpServer - MCP Server instance
625
+ * @param {Function} serverFactory - Builds an MCP Server for a given era
577
626
  * @param {object} config - HTTP transport configuration
578
627
  * @returns {Promise<HTTPTransportServer>}
579
628
  */
580
- export async function createHTTPTransport(mcpServer, config = {}) {
629
+ export async function createHTTPTransport(serverFactory, config = {}) {
581
630
  const httpTransport = new HTTPTransportServer(config);
582
- await httpTransport.initialize(mcpServer);
631
+ await httpTransport.initialize(serverFactory);
583
632
  return httpTransport;
584
633
  }