@ttoss/http-server-mcp 0.27.1 → 0.28.1
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 +123 -1
- package/dist/index.cjs +252 -37
- package/dist/index.d.cts +238 -4
- package/dist/index.d.mts +238 -4
- package/dist/index.mjs +251 -39
- package/package.json +1 -1
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
|
}
|
|
@@ -117,6 +170,160 @@ var createMcpRequestServer = ({
|
|
|
117
170
|
};
|
|
118
171
|
};
|
|
119
172
|
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/registerAppResource.ts
|
|
175
|
+
/**
|
|
176
|
+
* Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
|
|
177
|
+
* the key under `capabilities.extensions` on both sides of the handshake.
|
|
178
|
+
*
|
|
179
|
+
* A client declares Apps support with it; a server may advertise the same key
|
|
180
|
+
* so a host can discover the support without inspecting tool metadata:
|
|
181
|
+
*
|
|
182
|
+
* ```typescript
|
|
183
|
+
* new McpServer(
|
|
184
|
+
* { name: 'weather', version: '1.0.0' },
|
|
185
|
+
* {
|
|
186
|
+
* capabilities: {
|
|
187
|
+
* extensions: {
|
|
188
|
+
* [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
|
|
189
|
+
* },
|
|
190
|
+
* },
|
|
191
|
+
* }
|
|
192
|
+
* );
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
var UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
|
|
196
|
+
/** MIME type every MCP Apps HTML resource is served as. */
|
|
197
|
+
var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
198
|
+
/**
|
|
199
|
+
* Deprecated flat tool metadata key for the linked resource URI. Written
|
|
200
|
+
* alongside `_meta.ui.resourceUri` because both forms are legal until the
|
|
201
|
+
* extension reaches GA, at which point this key is removed from the spec.
|
|
202
|
+
*/
|
|
203
|
+
var LEGACY_RESOURCE_URI_KEY = "ui/resourceUri";
|
|
204
|
+
/**
|
|
205
|
+
* Registers an MCP Apps view: a `ui://` resource a tool result is rendered
|
|
206
|
+
* with, per the `io.modelcontextprotocol/ui` extension.
|
|
207
|
+
*
|
|
208
|
+
* The extension asks nothing of the transport — a view is an ordinary MCP
|
|
209
|
+
* resource, and the tool linkage is ordinary tool `_meta` — so this is
|
|
210
|
+
* registration sugar over `registerResource`, not new protocol surface. What
|
|
211
|
+
* it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
|
|
212
|
+
* MIME type, `_meta.ui` on both the declaration and the read result, and
|
|
213
|
+
* writing the deprecated flat linkage key alongside the current one.
|
|
214
|
+
*
|
|
215
|
+
* **Register the linkage unconditionally.** The extension's negotiation is
|
|
216
|
+
* per-request and only reliably readable on the `2026-07-28` revision; in this
|
|
217
|
+
* package's default stateless 2025-era mode there is no remembered
|
|
218
|
+
* `initialize` to read a client capability from. Gating registration on that
|
|
219
|
+
* capability would therefore silently drop the view for every 2025-era client.
|
|
220
|
+
* A host without Apps support simply ignores `_meta`, so keep each tool's
|
|
221
|
+
* `content` meaningful on its own and let the text result be the fallback.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* import {
|
|
226
|
+
* McpServer,
|
|
227
|
+
* registerAppResource,
|
|
228
|
+
* UI_EXTENSION_ID,
|
|
229
|
+
* UI_RESOURCE_MIME_TYPE,
|
|
230
|
+
* z,
|
|
231
|
+
* } from '@ttoss/http-server-mcp';
|
|
232
|
+
*
|
|
233
|
+
* const server = new McpServer(
|
|
234
|
+
* { name: 'weather', version: '1.0.0' },
|
|
235
|
+
* {
|
|
236
|
+
* capabilities: {
|
|
237
|
+
* extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
|
|
238
|
+
* },
|
|
239
|
+
* }
|
|
240
|
+
* );
|
|
241
|
+
*
|
|
242
|
+
* const dashboard = registerAppResource({
|
|
243
|
+
* server,
|
|
244
|
+
* name: 'weather_dashboard',
|
|
245
|
+
* uri: 'ui://weather/dashboard',
|
|
246
|
+
* description: 'Interactive weather dashboard',
|
|
247
|
+
* html: dashboardHtml,
|
|
248
|
+
* ui: {
|
|
249
|
+
* csp: { connectDomains: ['https://api.openweathermap.org'] },
|
|
250
|
+
* prefersBorder: true,
|
|
251
|
+
* },
|
|
252
|
+
* });
|
|
253
|
+
*
|
|
254
|
+
* server.registerTool(
|
|
255
|
+
* 'get-weather',
|
|
256
|
+
* {
|
|
257
|
+
* description: 'Get the weather for a location',
|
|
258
|
+
* inputSchema: { location: z.string() },
|
|
259
|
+
* _meta: dashboard.toolMeta(),
|
|
260
|
+
* },
|
|
261
|
+
* async ({ location }) => {
|
|
262
|
+
* const forecast = await fetchForecast(location);
|
|
263
|
+
* return {
|
|
264
|
+
* // Text stays meaningful: it is what a host without Apps renders.
|
|
265
|
+
* content: [{ type: 'text', text: summarise(forecast) }],
|
|
266
|
+
* structuredContent: forecast,
|
|
267
|
+
* };
|
|
268
|
+
* }
|
|
269
|
+
* );
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
var registerAppResource = ({
|
|
273
|
+
server,
|
|
274
|
+
name,
|
|
275
|
+
uri,
|
|
276
|
+
description,
|
|
277
|
+
html,
|
|
278
|
+
ui
|
|
279
|
+
}) => {
|
|
280
|
+
if (!uri.startsWith("ui://")) throw new Error(`An MCP Apps resource URI must use the ui:// scheme (received "${uri}").`);
|
|
281
|
+
const meta = ui === void 0 ? void 0 : {
|
|
282
|
+
ui
|
|
283
|
+
};
|
|
284
|
+
const resource = server.registerResource(name, uri, {
|
|
285
|
+
description,
|
|
286
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
287
|
+
...(meta === void 0 ? {} : {
|
|
288
|
+
_meta: meta
|
|
289
|
+
})
|
|
290
|
+
}, async requestedUri => {
|
|
291
|
+
const text = typeof html === "string" ? html : await html({
|
|
292
|
+
uri: requestedUri
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
contents: [{
|
|
296
|
+
uri: requestedUri.href,
|
|
297
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
298
|
+
text,
|
|
299
|
+
...(meta === void 0 ? {} : {
|
|
300
|
+
_meta: meta
|
|
301
|
+
})
|
|
302
|
+
}]
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
const toolMeta = ({
|
|
306
|
+
visibility,
|
|
307
|
+
_meta
|
|
308
|
+
} = {}) => {
|
|
309
|
+
return {
|
|
310
|
+
..._meta,
|
|
311
|
+
ui: {
|
|
312
|
+
resourceUri: uri,
|
|
313
|
+
...(visibility === void 0 ? {} : {
|
|
314
|
+
visibility
|
|
315
|
+
})
|
|
316
|
+
},
|
|
317
|
+
[LEGACY_RESOURCE_URI_KEY]: uri
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
uri,
|
|
322
|
+
resource,
|
|
323
|
+
toolMeta
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
120
327
|
//#endregion
|
|
121
328
|
//#region src/registerToolFromSchema.ts
|
|
122
329
|
/**
|
|
@@ -160,7 +367,7 @@ var toStandardSchema = ({
|
|
|
160
367
|
*
|
|
161
368
|
* @param server - The `McpServer` instance to register the tool on.
|
|
162
369
|
* @param params - Tool configuration including name, description, inputSchema,
|
|
163
|
-
* validateArguments, and handler.
|
|
370
|
+
* validateArguments, `_meta`, and handler.
|
|
164
371
|
*
|
|
165
372
|
* @example
|
|
166
373
|
* ```typescript
|
|
@@ -191,14 +398,16 @@ var registerToolFromSchema = (server, params) => {
|
|
|
191
398
|
properties: {}
|
|
192
399
|
},
|
|
193
400
|
validateArguments = false,
|
|
194
|
-
handler
|
|
401
|
+
handler,
|
|
402
|
+
_meta
|
|
195
403
|
} = params;
|
|
196
404
|
server.registerTool(name, {
|
|
197
405
|
description,
|
|
198
406
|
inputSchema: toStandardSchema({
|
|
199
407
|
inputSchema,
|
|
200
408
|
validateArguments
|
|
201
|
-
})
|
|
409
|
+
}),
|
|
410
|
+
_meta
|
|
202
411
|
}, async args => {
|
|
203
412
|
return handler(args);
|
|
204
413
|
});
|
|
@@ -313,7 +522,8 @@ var createGatedToolRegistrar = ({
|
|
|
313
522
|
};
|
|
314
523
|
server.registerTool(def.name, {
|
|
315
524
|
description: def.description,
|
|
316
|
-
inputSchema: def.inputSchema
|
|
525
|
+
inputSchema: def.inputSchema,
|
|
526
|
+
_meta: def._meta
|
|
317
527
|
}, handler);
|
|
318
528
|
};
|
|
319
529
|
return {
|
|
@@ -532,6 +742,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
532
742
|
path = "/mcp",
|
|
533
743
|
aliases = [],
|
|
534
744
|
sessionIdGenerator,
|
|
745
|
+
createMcpServer,
|
|
535
746
|
apiBaseUrl,
|
|
536
747
|
getApiHeaders,
|
|
537
748
|
auth
|
|
@@ -539,7 +750,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
539
750
|
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
540
751
|
const serveRequest = createMcpRequestServer({
|
|
541
752
|
server,
|
|
542
|
-
sessionIdGenerator
|
|
753
|
+
sessionIdGenerator,
|
|
754
|
+
createMcpServer
|
|
543
755
|
});
|
|
544
756
|
const router = new Router();
|
|
545
757
|
let authCheck;
|
|
@@ -633,4 +845,4 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
633
845
|
};
|
|
634
846
|
|
|
635
847
|
//#endregion
|
|
636
|
-
export { McpServer, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
|
|
848
|
+
export { McpServer, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerAppResource, registerToolFromSchema, z };
|