@ttoss/http-server-mcp 0.28.0 → 0.28.2
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/MIGRATIONS.md +35 -0
- package/README.md +34 -1
- package/dist/index.cjs +115 -35
- package/dist/index.d.cts +26 -1
- package/dist/index.d.mts +26 -1
- package/dist/index.mjs +116 -36
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
|
+
## The `2026-07-28` revision needs `createMcpServer`
|
|
4
|
+
|
|
5
|
+
That revision is served only when `createMcpRouter` is given a
|
|
6
|
+
`createMcpServer` factory.
|
|
7
|
+
|
|
8
|
+
```diff
|
|
9
|
+
+const buildServer = () => {
|
|
10
|
+
+ const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
11
|
+
+ registerEverything(mcpServer);
|
|
12
|
+
+ return mcpServer;
|
|
13
|
+
+};
|
|
14
|
+
+
|
|
15
|
+
-createMcpRouter(mcpServer, {
|
|
16
|
+
+createMcpRouter(buildServer(), {
|
|
17
|
+
+ createMcpServer: buildServer,
|
|
18
|
+
auth: { ... },
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Why it cannot default to the server you already pass.** The negotiated
|
|
23
|
+
revision is instance state: serving one `2026-07-28` request marks that
|
|
24
|
+
`McpServer` modern for good, and it then validates every later message against
|
|
25
|
+
that revision. One instance serving both eras is pinned by the first client to
|
|
26
|
+
speak the newer one, after which every 2025-era request is answered
|
|
27
|
+
`-32602 Request is missing the required _meta envelope for protocol revision
|
|
28
|
+
2026-07-28` at **HTTP 200** for the life of the process — a status and body a
|
|
29
|
+
client reads as "no tools" rather than as a fault. The SDK's own serving
|
|
30
|
+
entries take a factory and call it once per request for this reason.
|
|
31
|
+
|
|
32
|
+
**What you will observe if you miss this.** A client speaking `2026-07-28` gets
|
|
33
|
+
`400` with `-32022 Unsupported protocol version` and `data.supported` listing
|
|
34
|
+
the 2025-era revisions, which it can renegotiate from. Only deployments
|
|
35
|
+
actually receiving that traffic are affected; every client without the
|
|
36
|
+
per-request envelope is served exactly as before.
|
|
37
|
+
|
|
3
38
|
## `tools/list` is no longer public by default
|
|
4
39
|
|
|
5
40
|
`auth.publicMethods` now defaults to `['initialize']` instead of
|
package/README.md
CHANGED
|
@@ -127,6 +127,17 @@ const data = await apiCall('GET', 'https://partner.api.com/data', {
|
|
|
127
127
|
|
|
128
128
|
`apiCall` throws with a clear message when called with a relative path and no `apiBaseUrl` is configured in the context.
|
|
129
129
|
|
|
130
|
+
### Errors
|
|
131
|
+
|
|
132
|
+
A non-2xx response throws, with the message read off the error body — `{ error: 'text' }` and `{ error: { code, message } }` both work, the latter as `code: message`. A body naming neither falls back to `HTTP <status>`.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// { "error": { "code": "plan_limit_reached", "message": "Upgrade to add more." } }
|
|
136
|
+
// → Error: plan_limit_reached: Upgrade to add more.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Inside a tool handler the thrown message is what the MCP SDK returns to the client, so it is the whole answer the calling model acts on.
|
|
140
|
+
|
|
130
141
|
## Authentication
|
|
131
142
|
|
|
132
143
|
`createMcpRouter` supports OAuth 2.0 Bearer token authentication via the `auth` option. Incoming MCP requests must include a valid `Authorization: Bearer <token>` header — invalid or missing tokens receive a `401 Unauthorized` response. The MCP lifecycle methods `initialize` and `tools/list` are exempt by default so clients can discover the server before authenticating (see [Public methods and discovery](#public-methods-and-discovery)).
|
|
@@ -593,6 +604,8 @@ Generic HTTP helper for use inside MCP tool handlers.
|
|
|
593
604
|
|
|
594
605
|
**Returns:** `Promise<unknown>` — Parsed JSON response body
|
|
595
606
|
|
|
607
|
+
**Throws:** `Error` on a non-2xx response, carrying the error body's message (see [Errors](#errors))
|
|
608
|
+
|
|
596
609
|
### `getIdentity<T>()`
|
|
597
610
|
|
|
598
611
|
Returns the verified JWT payload for the current MCP request. Only available inside a tool handler when `auth` is configured. Returns `undefined` when called outside an authenticated context.
|
|
@@ -906,7 +919,27 @@ app.listen(3000);
|
|
|
906
919
|
|
|
907
920
|
This package implements the [Model Context Protocol](https://spec.modelcontextprotocol.io/) over HTTP using JSON responses (no SSE streaming), and serves each request over the protocol revision it actually speaks.
|
|
908
921
|
|
|
909
|
-
Requests are classified once at the boundary. Traffic from today's MCP clients is served over `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, adapting Koa's context-based middleware to the SDK's Node.js request/response expectations. Requests carrying the `2026-07-28` revision's per-request envelope are served by that revision's stateless core (`createMcpHandler`)
|
|
922
|
+
Requests are classified once at the boundary, and the classifier's answer is three-way. Traffic from today's MCP clients is served over `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, adapting Koa's context-based middleware to the SDK's Node.js request/response expectations. Requests carrying the `2026-07-28` revision's per-request envelope are served by that revision's stateless core (`createMcpHandler`). Requests the classifier refuses outright are answered with its own rejection — the status, code, message and data it chose — rather than passed to either era's handler.
|
|
923
|
+
|
|
924
|
+
### Serving the `2026-07-28` revision
|
|
925
|
+
|
|
926
|
+
Set `createMcpServer` to a factory returning an `McpServer` with the same tools registered:
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
const buildServer = () => {
|
|
930
|
+
const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
931
|
+
registerEverything(mcpServer);
|
|
932
|
+
return mcpServer;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const mcpRouter = createMcpRouter(buildServer(), {
|
|
936
|
+
createMcpServer: buildServer,
|
|
937
|
+
});
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
It has to be a factory, and cannot default to the server the router was given, because the negotiated revision is instance state: serving one `2026-07-28` request marks that `McpServer` modern for good, and it then validates every later message against that revision. One instance serving both eras is pinned by the first client to speak the newer one, after which every 2025-era request is answered `-32602 Request is missing the required _meta envelope…` at HTTP 200 for the life of the process. The SDK's own serving entries take a factory and call it once per request for the same reason.
|
|
941
|
+
|
|
942
|
+
Without `createMcpServer`, `2026-07-28` requests get the unsupported-protocol-version error listing the revisions this endpoint does serve, so that client renegotiates and no other client is affected.
|
|
910
943
|
|
|
911
944
|
**Supported HTTP methods:**
|
|
912
945
|
|
package/dist/index.cjs
CHANGED
|
@@ -36,41 +36,88 @@ var header = (ctx, name) => {
|
|
|
36
36
|
const value = ctx.get(name);
|
|
37
37
|
return value === "" ? void 0 : value;
|
|
38
38
|
};
|
|
39
|
+
/** The id to echo in an error answer, by the SDK's own rule. */
|
|
40
|
+
var echoableRequestId = body => {
|
|
41
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
42
|
+
const {
|
|
43
|
+
id,
|
|
44
|
+
method
|
|
45
|
+
} = body;
|
|
46
|
+
if (typeof method !== "string") return null;
|
|
47
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Writes a JSON-RPC error straight to the Node response: `createMcpRouter` sets
|
|
51
|
+
* `ctx.respond = false` after serving, so Koa never writes `ctx.body`.
|
|
52
|
+
*/
|
|
53
|
+
var emitJsonRpcError = (ctx, answer, body) => {
|
|
54
|
+
ctx.res.writeHead(answer.httpStatus, {
|
|
55
|
+
"content-type": "application/json"
|
|
56
|
+
});
|
|
57
|
+
ctx.res.end(JSON.stringify({
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
error: {
|
|
60
|
+
code: answer.code,
|
|
61
|
+
message: answer.message,
|
|
62
|
+
...(answer.data !== void 0 && {
|
|
63
|
+
data: answer.data
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
id: echoableRequestId(body)
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
/** The SDK's own ladder rejection, emitted with the status the SDK chose. */
|
|
70
|
+
var rejectionAnswer = rejection => {
|
|
71
|
+
return {
|
|
72
|
+
httpStatus: rejection.httpStatus,
|
|
73
|
+
code: rejection.code,
|
|
74
|
+
message: rejection.message,
|
|
75
|
+
...(rejection.data !== void 0 && {
|
|
76
|
+
data: rejection.data
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The answer to `2026-07-28` traffic without `createMcpServer`. `data.supported`
|
|
82
|
+
* is what lets the client renegotiate onto a revision this endpoint serves.
|
|
83
|
+
*/
|
|
84
|
+
var REVISION_NOT_SERVED_ANSWER = {
|
|
85
|
+
httpStatus: 400,
|
|
86
|
+
code: _modelcontextprotocol_server.ProtocolErrorCode.UnsupportedProtocolVersion,
|
|
87
|
+
message: "Unsupported protocol version: this endpoint serves the 2025-era MCP revisions only. Pass createMcpServer to createMcpRouter to serve protocol revision 2026-07-28.",
|
|
88
|
+
data: {
|
|
89
|
+
supported: [..._modelcontextprotocol_server.SUPPORTED_PROTOCOL_VERSIONS]
|
|
90
|
+
}
|
|
91
|
+
};
|
|
39
92
|
/**
|
|
40
93
|
* Builds the per-request serving function used by `createMcpRouter`, routing
|
|
41
|
-
* each request
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* both eras through its built-in legacy fallback — is what keeps 2025-era
|
|
59
|
-
* responses byte-compatible. That fallback constructs its transport with only
|
|
60
|
-
* `sessionIdGenerator: undefined` and no way to pass `enableJsonResponse`, so
|
|
61
|
-
* every response it produces is SSE-framed (`text/event-stream`) regardless of
|
|
62
|
-
* the handler's `responseMode`. Owning the legacy branch keeps `application/json`.
|
|
94
|
+
* each request by the SDK's three-way `classifyInboundRequest` outcome.
|
|
95
|
+
*
|
|
96
|
+
* **Why the modern era needs `createMcpServer` rather than the `server` above.**
|
|
97
|
+
* The negotiated revision is *instance* state: the SDK marks an instance modern
|
|
98
|
+
* when it serves one modern request, and it then validates every later message
|
|
99
|
+
* against `2026-07-28` — there is no per-request era consult. One instance
|
|
100
|
+
* serving both eras is therefore pinned by the first client to speak the newer
|
|
101
|
+
* revision, after which every 2025-era request is answered `-32602 Request is
|
|
102
|
+
* missing the required _meta envelope…` at HTTP 200 for the life of the
|
|
103
|
+
* process. The SDK's own entries take a factory and call it per request for
|
|
104
|
+
* this reason (they also `close()` the instance, which a shared one cannot
|
|
105
|
+
* survive). Without the factory, `2026-07-28` traffic gets
|
|
106
|
+
* {@link REVISION_NOT_SERVED_ANSWER} and no other client is affected.
|
|
107
|
+
*
|
|
108
|
+
* **Why not `createMcpHandler`'s built-in legacy fallback.** It constructs its
|
|
109
|
+
* transport with no way to pass `enableJsonResponse`, so every response it
|
|
110
|
+
* produces is SSE-framed. Owning the legacy branch keeps `application/json`.
|
|
63
111
|
*/
|
|
64
112
|
var createMcpRequestServer = ({
|
|
113
|
+
createMcpServer,
|
|
65
114
|
server,
|
|
66
115
|
sessionIdGenerator
|
|
67
116
|
}) => {
|
|
68
117
|
const isStateful = sessionIdGenerator !== void 0;
|
|
69
|
-
const modernHandler = (0, _modelcontextprotocol_node.toNodeHandler)((0, _modelcontextprotocol_server.createMcpHandler)(
|
|
70
|
-
return server;
|
|
71
|
-
}, {
|
|
118
|
+
const modernHandler = createMcpServer ? (0, _modelcontextprotocol_node.toNodeHandler)((0, _modelcontextprotocol_server.createMcpHandler)(createMcpServer, {
|
|
72
119
|
legacy: "reject"
|
|
73
|
-
}));
|
|
120
|
+
})) : void 0;
|
|
74
121
|
let sharedTransport;
|
|
75
122
|
if (isStateful) {
|
|
76
123
|
sharedTransport = new _modelcontextprotocol_node.NodeStreamableHTTPServerTransport({
|
|
@@ -87,17 +134,23 @@ var createMcpRequestServer = ({
|
|
|
87
134
|
statelessQueue = result.catch(() => {});
|
|
88
135
|
return result;
|
|
89
136
|
};
|
|
90
|
-
|
|
91
|
-
|
|
137
|
+
return async (ctx, body) => {
|
|
138
|
+
const outcome = (0, _modelcontextprotocol_server.classifyInboundRequest)({
|
|
92
139
|
httpMethod: ctx.method,
|
|
93
140
|
protocolVersionHeader: header(ctx, "mcp-protocol-version"),
|
|
94
141
|
mcpMethodHeader: header(ctx, "mcp-method"),
|
|
95
142
|
mcpNameHeader: header(ctx, "mcp-name"),
|
|
96
143
|
body
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
144
|
+
});
|
|
145
|
+
if (outcome.kind === "reject") {
|
|
146
|
+
emitJsonRpcError(ctx, rejectionAnswer(outcome), body);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (outcome.kind === "modern") {
|
|
150
|
+
if (modernHandler === void 0) {
|
|
151
|
+
emitJsonRpcError(ctx, REVISION_NOT_SERVED_ANSWER, body);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
101
154
|
await modernHandler(ctx.req, ctx.res, body);
|
|
102
155
|
return;
|
|
103
156
|
}
|
|
@@ -535,6 +588,31 @@ var buildVerifyToken = auth => {
|
|
|
535
588
|
return payload;
|
|
536
589
|
};
|
|
537
590
|
};
|
|
591
|
+
var isRecord = value => {
|
|
592
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
593
|
+
};
|
|
594
|
+
var readText = value => {
|
|
595
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
596
|
+
};
|
|
597
|
+
/**
|
|
598
|
+
* The message an error body carries, across the two shapes REST APIs answer
|
|
599
|
+
* with: `{ error: 'text' }` and `{ error: { code?, message? } }`.
|
|
600
|
+
*
|
|
601
|
+
* The structured one is why this is read rather than taken: an object handed
|
|
602
|
+
* to `new Error()` renders as `[object Object]`, and inside a tool handler
|
|
603
|
+
* that string is the whole answer the calling model acts on.
|
|
604
|
+
*/
|
|
605
|
+
var errorBodyMessage = body => {
|
|
606
|
+
if (!isRecord(body)) return;
|
|
607
|
+
const {
|
|
608
|
+
error
|
|
609
|
+
} = body;
|
|
610
|
+
if (!isRecord(error)) return readText(error);
|
|
611
|
+
const code = readText(error.code);
|
|
612
|
+
const message = readText(error.message);
|
|
613
|
+
if (message === void 0) return code;
|
|
614
|
+
return code === void 0 ? message : `${code}: ${message}`;
|
|
615
|
+
};
|
|
538
616
|
/**
|
|
539
617
|
* Generic HTTP helper for use inside MCP tool handlers.
|
|
540
618
|
*
|
|
@@ -610,12 +688,12 @@ var apiCall = async (method, url, options) => {
|
|
|
610
688
|
body: hasBody ? JSON.stringify(options.body) : void 0
|
|
611
689
|
});
|
|
612
690
|
if (!response.ok) {
|
|
613
|
-
const
|
|
691
|
+
const errorBody = await response.json().catch(() => {
|
|
614
692
|
return {
|
|
615
693
|
error: response.statusText
|
|
616
694
|
};
|
|
617
695
|
});
|
|
618
|
-
throw new Error(
|
|
696
|
+
throw new Error(errorBodyMessage(errorBody) ?? `HTTP ${response.status}`);
|
|
619
697
|
}
|
|
620
698
|
if (response.status === 204 || response.status === 205) return;
|
|
621
699
|
if (response.headers.get("content-type")?.includes("application/json")) return response.json();
|
|
@@ -692,6 +770,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
692
770
|
path = "/mcp",
|
|
693
771
|
aliases = [],
|
|
694
772
|
sessionIdGenerator,
|
|
773
|
+
createMcpServer,
|
|
695
774
|
apiBaseUrl,
|
|
696
775
|
getApiHeaders,
|
|
697
776
|
auth
|
|
@@ -699,7 +778,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
699
778
|
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
700
779
|
const serveRequest = createMcpRequestServer({
|
|
701
780
|
server,
|
|
702
|
-
sessionIdGenerator
|
|
781
|
+
sessionIdGenerator,
|
|
782
|
+
createMcpServer
|
|
703
783
|
});
|
|
704
784
|
const router = new _ttoss_http_server.Router();
|
|
705
785
|
let authCheck;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { CallToolResult, McpServer, McpServer as McpServer$1, RegisteredResource } from "@modelcontextprotocol/server";
|
|
3
|
+
import { CallToolResult, McpServer, McpServer as McpServer$1, McpServerFactory, RegisteredResource } from "@modelcontextprotocol/server";
|
|
4
4
|
import { Router } from "@ttoss/http-server";
|
|
5
5
|
import accepts from "accepts";
|
|
6
6
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -1310,6 +1310,31 @@ interface McpRouterOptions {
|
|
|
1310
1310
|
* served statelessly regardless of this option.
|
|
1311
1311
|
*/
|
|
1312
1312
|
sessionIdGenerator?: () => string;
|
|
1313
|
+
/**
|
|
1314
|
+
* Per-request factory for the `McpServer` serving one `2026-07-28` request.
|
|
1315
|
+
* Set it to serve that revision; without it, requests carrying its
|
|
1316
|
+
* per-request envelope get the unsupported-protocol-version error naming the
|
|
1317
|
+
* 2025-era revisions this endpoint does serve. Register the same tools here
|
|
1318
|
+
* as on `server`, so the two eras cannot drift apart.
|
|
1319
|
+
*
|
|
1320
|
+
* It cannot default to `server`: the negotiated revision is instance state,
|
|
1321
|
+
* so one instance serving both eras is pinned to `2026-07-28` by the first
|
|
1322
|
+
* client to speak it, and every 2025-era request after that is answered
|
|
1323
|
+
* `-32602 … missing the required _meta envelope` at HTTP 200. The SDK's own
|
|
1324
|
+
* serving entries take a factory and call it once per request.
|
|
1325
|
+
*
|
|
1326
|
+
* @example
|
|
1327
|
+
* ```typescript
|
|
1328
|
+
* const buildServer = () => {
|
|
1329
|
+
* const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
1330
|
+
* registerEverything(mcpServer);
|
|
1331
|
+
* return mcpServer;
|
|
1332
|
+
* };
|
|
1333
|
+
*
|
|
1334
|
+
* createMcpRouter(buildServer(), { createMcpServer: buildServer });
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
createMcpServer?: McpServerFactory;
|
|
1313
1338
|
/**
|
|
1314
1339
|
* Base URL prepended to relative paths passed to `apiCall` (paths starting
|
|
1315
1340
|
* with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
|
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { Router } from "@ttoss/http-server";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import { CallToolResult, McpServer, McpServer as McpServer$1, RegisteredResource } from "@modelcontextprotocol/server";
|
|
5
|
+
import { CallToolResult, McpServer, McpServer as McpServer$1, McpServerFactory, RegisteredResource } from "@modelcontextprotocol/server";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import accepts from "accepts";
|
|
8
8
|
import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
|
|
@@ -1311,6 +1311,31 @@ interface McpRouterOptions {
|
|
|
1311
1311
|
* served statelessly regardless of this option.
|
|
1312
1312
|
*/
|
|
1313
1313
|
sessionIdGenerator?: () => string;
|
|
1314
|
+
/**
|
|
1315
|
+
* Per-request factory for the `McpServer` serving one `2026-07-28` request.
|
|
1316
|
+
* Set it to serve that revision; without it, requests carrying its
|
|
1317
|
+
* per-request envelope get the unsupported-protocol-version error naming the
|
|
1318
|
+
* 2025-era revisions this endpoint does serve. Register the same tools here
|
|
1319
|
+
* as on `server`, so the two eras cannot drift apart.
|
|
1320
|
+
*
|
|
1321
|
+
* It cannot default to `server`: the negotiated revision is instance state,
|
|
1322
|
+
* so one instance serving both eras is pinned to `2026-07-28` by the first
|
|
1323
|
+
* client to speak it, and every 2025-era request after that is answered
|
|
1324
|
+
* `-32602 … missing the required _meta envelope` at HTTP 200. The SDK's own
|
|
1325
|
+
* serving entries take a factory and call it once per request.
|
|
1326
|
+
*
|
|
1327
|
+
* @example
|
|
1328
|
+
* ```typescript
|
|
1329
|
+
* const buildServer = () => {
|
|
1330
|
+
* const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
1331
|
+
* registerEverything(mcpServer);
|
|
1332
|
+
* return mcpServer;
|
|
1333
|
+
* };
|
|
1334
|
+
*
|
|
1335
|
+
* createMcpRouter(buildServer(), { createMcpServer: buildServer });
|
|
1336
|
+
* ```
|
|
1337
|
+
*/
|
|
1338
|
+
createMcpServer?: McpServerFactory;
|
|
1314
1339
|
/**
|
|
1315
1340
|
* Base URL prepended to relative paths passed to `apiCall` (paths starting
|
|
1316
1341
|
* with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { Router } from "@ttoss/http-server";
|
|
|
5
5
|
import { authMiddleware } from "@ttoss/http-server-auth";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { NodeStreamableHTTPServerTransport, toNodeHandler } from "@modelcontextprotocol/node";
|
|
8
|
-
import { McpServer, classifyInboundRequest, createMcpHandler, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
8
|
+
import { McpServer, ProtocolErrorCode, SUPPORTED_PROTOCOL_VERSIONS, classifyInboundRequest, createMcpHandler, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
|
|
11
11
|
//#region src/context.ts
|
|
@@ -33,41 +33,88 @@ var header = (ctx, name) => {
|
|
|
33
33
|
const value = ctx.get(name);
|
|
34
34
|
return value === "" ? void 0 : value;
|
|
35
35
|
};
|
|
36
|
+
/** The id to echo in an error answer, by the SDK's own rule. */
|
|
37
|
+
var echoableRequestId = body => {
|
|
38
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
39
|
+
const {
|
|
40
|
+
id,
|
|
41
|
+
method
|
|
42
|
+
} = body;
|
|
43
|
+
if (typeof method !== "string") return null;
|
|
44
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Writes a JSON-RPC error straight to the Node response: `createMcpRouter` sets
|
|
48
|
+
* `ctx.respond = false` after serving, so Koa never writes `ctx.body`.
|
|
49
|
+
*/
|
|
50
|
+
var emitJsonRpcError = (ctx, answer, body) => {
|
|
51
|
+
ctx.res.writeHead(answer.httpStatus, {
|
|
52
|
+
"content-type": "application/json"
|
|
53
|
+
});
|
|
54
|
+
ctx.res.end(JSON.stringify({
|
|
55
|
+
jsonrpc: "2.0",
|
|
56
|
+
error: {
|
|
57
|
+
code: answer.code,
|
|
58
|
+
message: answer.message,
|
|
59
|
+
...(answer.data !== void 0 && {
|
|
60
|
+
data: answer.data
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
id: echoableRequestId(body)
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
/** The SDK's own ladder rejection, emitted with the status the SDK chose. */
|
|
67
|
+
var rejectionAnswer = rejection => {
|
|
68
|
+
return {
|
|
69
|
+
httpStatus: rejection.httpStatus,
|
|
70
|
+
code: rejection.code,
|
|
71
|
+
message: rejection.message,
|
|
72
|
+
...(rejection.data !== void 0 && {
|
|
73
|
+
data: rejection.data
|
|
74
|
+
})
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The answer to `2026-07-28` traffic without `createMcpServer`. `data.supported`
|
|
79
|
+
* is what lets the client renegotiate onto a revision this endpoint serves.
|
|
80
|
+
*/
|
|
81
|
+
var REVISION_NOT_SERVED_ANSWER = {
|
|
82
|
+
httpStatus: 400,
|
|
83
|
+
code: ProtocolErrorCode.UnsupportedProtocolVersion,
|
|
84
|
+
message: "Unsupported protocol version: this endpoint serves the 2025-era MCP revisions only. Pass createMcpServer to createMcpRouter to serve protocol revision 2026-07-28.",
|
|
85
|
+
data: {
|
|
86
|
+
supported: [...SUPPORTED_PROTOCOL_VERSIONS]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
36
89
|
/**
|
|
37
90
|
* Builds the per-request serving function used by `createMcpRouter`, routing
|
|
38
|
-
* each request
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* both eras through its built-in legacy fallback — is what keeps 2025-era
|
|
56
|
-
* responses byte-compatible. That fallback constructs its transport with only
|
|
57
|
-
* `sessionIdGenerator: undefined` and no way to pass `enableJsonResponse`, so
|
|
58
|
-
* every response it produces is SSE-framed (`text/event-stream`) regardless of
|
|
59
|
-
* the handler's `responseMode`. Owning the legacy branch keeps `application/json`.
|
|
91
|
+
* each request by the SDK's three-way `classifyInboundRequest` outcome.
|
|
92
|
+
*
|
|
93
|
+
* **Why the modern era needs `createMcpServer` rather than the `server` above.**
|
|
94
|
+
* The negotiated revision is *instance* state: the SDK marks an instance modern
|
|
95
|
+
* when it serves one modern request, and it then validates every later message
|
|
96
|
+
* against `2026-07-28` — there is no per-request era consult. One instance
|
|
97
|
+
* serving both eras is therefore pinned by the first client to speak the newer
|
|
98
|
+
* revision, after which every 2025-era request is answered `-32602 Request is
|
|
99
|
+
* missing the required _meta envelope…` at HTTP 200 for the life of the
|
|
100
|
+
* process. The SDK's own entries take a factory and call it per request for
|
|
101
|
+
* this reason (they also `close()` the instance, which a shared one cannot
|
|
102
|
+
* survive). Without the factory, `2026-07-28` traffic gets
|
|
103
|
+
* {@link REVISION_NOT_SERVED_ANSWER} and no other client is affected.
|
|
104
|
+
*
|
|
105
|
+
* **Why not `createMcpHandler`'s built-in legacy fallback.** It constructs its
|
|
106
|
+
* transport with no way to pass `enableJsonResponse`, so every response it
|
|
107
|
+
* produces is SSE-framed. Owning the legacy branch keeps `application/json`.
|
|
60
108
|
*/
|
|
61
109
|
var createMcpRequestServer = ({
|
|
110
|
+
createMcpServer,
|
|
62
111
|
server,
|
|
63
112
|
sessionIdGenerator
|
|
64
113
|
}) => {
|
|
65
114
|
const isStateful = sessionIdGenerator !== void 0;
|
|
66
|
-
const modernHandler = toNodeHandler(createMcpHandler(
|
|
67
|
-
return server;
|
|
68
|
-
}, {
|
|
115
|
+
const modernHandler = createMcpServer ? toNodeHandler(createMcpHandler(createMcpServer, {
|
|
69
116
|
legacy: "reject"
|
|
70
|
-
}));
|
|
117
|
+
})) : void 0;
|
|
71
118
|
let sharedTransport;
|
|
72
119
|
if (isStateful) {
|
|
73
120
|
sharedTransport = new NodeStreamableHTTPServerTransport({
|
|
@@ -84,17 +131,23 @@ var createMcpRequestServer = ({
|
|
|
84
131
|
statelessQueue = result.catch(() => {});
|
|
85
132
|
return result;
|
|
86
133
|
};
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
return async (ctx, body) => {
|
|
135
|
+
const outcome = classifyInboundRequest({
|
|
89
136
|
httpMethod: ctx.method,
|
|
90
137
|
protocolVersionHeader: header(ctx, "mcp-protocol-version"),
|
|
91
138
|
mcpMethodHeader: header(ctx, "mcp-method"),
|
|
92
139
|
mcpNameHeader: header(ctx, "mcp-name"),
|
|
93
140
|
body
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
141
|
+
});
|
|
142
|
+
if (outcome.kind === "reject") {
|
|
143
|
+
emitJsonRpcError(ctx, rejectionAnswer(outcome), body);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (outcome.kind === "modern") {
|
|
147
|
+
if (modernHandler === void 0) {
|
|
148
|
+
emitJsonRpcError(ctx, REVISION_NOT_SERVED_ANSWER, body);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
98
151
|
await modernHandler(ctx.req, ctx.res, body);
|
|
99
152
|
return;
|
|
100
153
|
}
|
|
@@ -532,6 +585,31 @@ var buildVerifyToken = auth => {
|
|
|
532
585
|
return payload;
|
|
533
586
|
};
|
|
534
587
|
};
|
|
588
|
+
var isRecord = value => {
|
|
589
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
590
|
+
};
|
|
591
|
+
var readText = value => {
|
|
592
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* The message an error body carries, across the two shapes REST APIs answer
|
|
596
|
+
* with: `{ error: 'text' }` and `{ error: { code?, message? } }`.
|
|
597
|
+
*
|
|
598
|
+
* The structured one is why this is read rather than taken: an object handed
|
|
599
|
+
* to `new Error()` renders as `[object Object]`, and inside a tool handler
|
|
600
|
+
* that string is the whole answer the calling model acts on.
|
|
601
|
+
*/
|
|
602
|
+
var errorBodyMessage = body => {
|
|
603
|
+
if (!isRecord(body)) return;
|
|
604
|
+
const {
|
|
605
|
+
error
|
|
606
|
+
} = body;
|
|
607
|
+
if (!isRecord(error)) return readText(error);
|
|
608
|
+
const code = readText(error.code);
|
|
609
|
+
const message = readText(error.message);
|
|
610
|
+
if (message === void 0) return code;
|
|
611
|
+
return code === void 0 ? message : `${code}: ${message}`;
|
|
612
|
+
};
|
|
535
613
|
/**
|
|
536
614
|
* Generic HTTP helper for use inside MCP tool handlers.
|
|
537
615
|
*
|
|
@@ -607,12 +685,12 @@ var apiCall = async (method, url, options) => {
|
|
|
607
685
|
body: hasBody ? JSON.stringify(options.body) : void 0
|
|
608
686
|
});
|
|
609
687
|
if (!response.ok) {
|
|
610
|
-
const
|
|
688
|
+
const errorBody = await response.json().catch(() => {
|
|
611
689
|
return {
|
|
612
690
|
error: response.statusText
|
|
613
691
|
};
|
|
614
692
|
});
|
|
615
|
-
throw new Error(
|
|
693
|
+
throw new Error(errorBodyMessage(errorBody) ?? `HTTP ${response.status}`);
|
|
616
694
|
}
|
|
617
695
|
if (response.status === 204 || response.status === 205) return;
|
|
618
696
|
if (response.headers.get("content-type")?.includes("application/json")) return response.json();
|
|
@@ -689,6 +767,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
689
767
|
path = "/mcp",
|
|
690
768
|
aliases = [],
|
|
691
769
|
sessionIdGenerator,
|
|
770
|
+
createMcpServer,
|
|
692
771
|
apiBaseUrl,
|
|
693
772
|
getApiHeaders,
|
|
694
773
|
auth
|
|
@@ -696,7 +775,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
696
775
|
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
697
776
|
const serveRequest = createMcpRequestServer({
|
|
698
777
|
server,
|
|
699
|
-
sessionIdGenerator
|
|
778
|
+
sessionIdGenerator,
|
|
779
|
+
createMcpServer
|
|
700
780
|
});
|
|
701
781
|
const router = new Router();
|
|
702
782
|
let authCheck;
|