@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/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 to the protocol revision it actually speaks.
39
- *
40
- * Requests are classified once at the boundary with the SDK's own
41
- * `classifyInboundRequest`:
42
- *
43
- * - **2025-era traffic** (everything without a `2026-07-28` per-request
44
- * envelope — which is all traffic from today's MCP clients) is served over
45
- * `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, the
46
- * same wiring and the same plain-JSON responses this package has always
47
- * produced.
48
- * - **`2026-07-28` traffic** is served by `createMcpHandler` with
49
- * `legacy: 'reject'`, so the modern revision is handled by the SDK's
50
- * stateless core rather than the transport above. Malformed requests are
51
- * routed here too, so the SDK emits its own spec-defined rejection instead of
52
- * this package inventing one.
53
- *
54
- * Splitting on the classifier — instead of letting `createMcpHandler` serve
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
- const isLegacy = (ctx, body) => {
88
- return classifyInboundRequest({
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
- }).kind === "legacy";
95
- };
96
- return async (ctx, body) => {
97
- if (!isLegacy(ctx, body)) {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.27.1",
3
+ "version": "0.28.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",