@ttoss/http-server-mcp 0.23.1 → 0.24.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/README.md CHANGED
@@ -593,10 +593,17 @@ Use this when tool definitions are shared between the MCP server and an AI SDK a
593
593
  - `params.name` (`string`) — Unique tool name
594
594
  - `params.description` (`string`, optional) — Human-readable description
595
595
  - `params.inputSchema` (`JsonObjectSchema`, optional) — Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
596
+ - `params.validateArguments` (`boolean`, optional) — Enforce `inputSchema` on `tools/call` (default: `false`); see [Argument validation](#argument-validation)
596
597
  - `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) — Tool handler receiving the raw request arguments
597
598
 
598
599
  **Returns:** `void`
599
600
 
601
+ #### Argument validation
602
+
603
+ By default, `inputSchema` is **advertised but not enforced**: it round-trips verbatim over `tools/list` so clients know what to send, while arguments reach your handler unchecked. Pass `validateArguments: true` to reject mismatched calls with an MCP error before the handler runs.
604
+
605
+ Enable it once you're confident `inputSchema` describes every value the tool genuinely accepts. Schemas derived from an OpenAPI document are a common source of _incomplete_ ones — a field a client may send as `null` to clear it, or one that accepts several shapes, is easily emitted as a bare `{ type: 'string' }`. Validating against a schema like that rejects calls the underlying API would have accepted. To describe those cases accurately, use `{ type: ['string', 'null'] }` for a nullable field and forward `oneOf`/`anyOf` verbatim rather than collapsing to one type.
606
+
600
607
  ## Examples
601
608
 
602
609
  ### Plain JSON Schema Tool (`registerToolFromSchema`)
@@ -787,7 +794,9 @@ app.listen(3000);
787
794
 
788
795
  ## Protocol Details
789
796
 
790
- This package implements the [Model Context Protocol](https://spec.modelcontextprotocol.io/) over HTTP using JSON responses (no SSE streaming). It uses the `StreamableHTTPServerTransport` from the MCP SDK with `enableJsonResponse: true` and adapts Koa's context-based middleware to work with the MCP SDK's Node.js request/response expectations.
797
+ 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.
798
+
799
+ 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`) instead. Both paths are served from the same `McpServer` instance and the same registered tools, so supporting the newer revision requires no configuration.
791
800
 
792
801
  **Supported HTTP methods:**
793
802
 
@@ -812,6 +821,8 @@ If you authenticate with `auth.verifyToken`, you do not need `sessionIdGenerator
812
821
  | Stateless (default) | Bearer/API tokens, serverless, multi-instance | DB/verify hit per request |
813
822
  | Stateful (`sessionIdGenerator`) | SSE events, context-preserving streams | Needs session affinity / shared state |
814
823
 
824
+ The `2026-07-28` revision has no session concept in its core, so requests speaking that revision are always served statelessly regardless of `sessionIdGenerator`.
825
+
815
826
  ## Testing an MCP server
816
827
 
817
828
  MCP requests are plain JSON-RPC POSTs to the router path. The `initialize` and `tools/list` methods are public by default, so they need no auth header; `tools/call` runs through `auth.verifyToken`. The client must send `Accept: application/json, text/event-stream` — the transport rejects requests that do not accept the event-stream media type.
@@ -864,7 +875,7 @@ Use the default when token auth is handled outside the OAuth flow (Cognito, API
864
875
 
865
876
  The default **stateless** mode (see [Stateless vs stateful mode](#stateless-vs-stateful-mode)) is built for serverless: a fresh transport is created per request, nothing is kept in memory between invocations, and responses are plain JSON (no SSE) — exactly the request/response shape API Gateway and Lambda Function URLs expect.
866
877
 
867
- Use [`@ttoss/http-server-serverless`](https://github.com/ttoss/ttoss/tree/main/packages/http-server-serverless) as the Lambda adapter. It wraps `serverless-http` and additionally populates `req.rawHeaders` from the API Gateway event before the request reaches Koa. Without this step, `@hono/node-server` — used internally by the MCP transport — drops all headers (including `Accept`) and every `initialize` request returns HTTP 406.
878
+ Use [`@ttoss/http-server-serverless`](https://github.com/ttoss/ttoss/tree/main/packages/http-server-serverless) as the Lambda adapter. It wraps `serverless-http` and additionally populates `req.rawHeaders` from the API Gateway event before the request reaches Koa. Without this step, the adapter sitting between Koa and the MCP transport can drop all headers (including `Accept`), making every `initialize` request return HTTP 406.
868
879
 
869
880
  ```mermaid
870
881
  flowchart LR
@@ -913,7 +924,8 @@ Keep the following in mind:
913
924
 
914
925
  - [@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server) - HTTP server foundation
915
926
  - [@ttoss/http-server-serverless](https://github.com/ttoss/ttoss/tree/main/packages/http-server-serverless) - AWS Lambda adapter (required for MCP on Lambda)
916
- - [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk) - MCP SDK
927
+ - [@modelcontextprotocol/server](https://github.com/modelcontextprotocol/typescript-sdk) - MCP server SDK (2026-07-28 core)
928
+ - [@modelcontextprotocol/node](https://github.com/modelcontextprotocol/typescript-sdk) - Node.js request/response bridge for the SDK's fetch-based handler
917
929
 
918
930
  ## Resources
919
931
 
package/dist/index.cjs CHANGED
@@ -2,13 +2,13 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
- let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
6
5
  let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
7
6
  let _ttoss_http_server = require("@ttoss/http-server");
8
7
  let _ttoss_http_server_auth = require("@ttoss/http-server-auth");
9
8
  let node_async_hooks = require("node:async_hooks");
9
+ let _modelcontextprotocol_node = require("@modelcontextprotocol/node");
10
+ let _modelcontextprotocol_server = require("@modelcontextprotocol/server");
10
11
  let zod = require("zod");
11
- let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
12
12
 
13
13
  //#region src/context.ts
14
14
  var requestContextStore = new node_async_hooks.AsyncLocalStorage();
@@ -24,30 +24,145 @@ var getIdentity = () => {
24
24
  return requestContextStore.getStore()?.identity;
25
25
  };
26
26
 
27
+ //#endregion
28
+ //#region src/serveMcpRequest.ts
29
+ /**
30
+ * Reads a header as `string | undefined`. Koa's `ctx.get` returns `''` for a
31
+ * missing header, which the classifier would read as "header present but
32
+ * empty" rather than absent.
33
+ */
34
+ var header = (ctx, name) => {
35
+ const value = ctx.get(name);
36
+ return value === "" ? void 0 : value;
37
+ };
38
+ /**
39
+ * Builds the per-request serving function used by `createMcpRouter`, routing
40
+ * each request to the protocol revision it actually speaks.
41
+ *
42
+ * Requests are classified once at the boundary with the SDK's own
43
+ * `classifyInboundRequest`:
44
+ *
45
+ * - **2025-era traffic** (everything without a `2026-07-28` per-request
46
+ * envelope — which is all traffic from today's MCP clients) is served over
47
+ * `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, the
48
+ * same wiring and the same plain-JSON responses this package has always
49
+ * produced.
50
+ * - **`2026-07-28` traffic** is served by `createMcpHandler` with
51
+ * `legacy: 'reject'`, so the modern revision is handled by the SDK's
52
+ * stateless core rather than the transport above. Malformed requests are
53
+ * routed here too, so the SDK emits its own spec-defined rejection instead of
54
+ * this package inventing one.
55
+ *
56
+ * Splitting on the classifier — instead of letting `createMcpHandler` serve
57
+ * both eras through its built-in legacy fallback — is what keeps 2025-era
58
+ * responses byte-compatible. That fallback constructs its transport with only
59
+ * `sessionIdGenerator: undefined` and no way to pass `enableJsonResponse`, so
60
+ * every response it produces is SSE-framed (`text/event-stream`) regardless of
61
+ * the handler's `responseMode`. Owning the legacy branch keeps `application/json`.
62
+ */
63
+ var createMcpRequestServer = ({
64
+ server,
65
+ sessionIdGenerator
66
+ }) => {
67
+ const isStateful = sessionIdGenerator !== void 0;
68
+ const modernHandler = (0, _modelcontextprotocol_node.toNodeHandler)((0, _modelcontextprotocol_server.createMcpHandler)(() => {
69
+ return server;
70
+ }, {
71
+ legacy: "reject"
72
+ }));
73
+ let sharedTransport;
74
+ if (isStateful) {
75
+ sharedTransport = new _modelcontextprotocol_node.NodeStreamableHTTPServerTransport({
76
+ sessionIdGenerator,
77
+ enableJsonResponse: true
78
+ });
79
+ server.connect(sharedTransport);
80
+ }
81
+ let statelessQueue = Promise.resolve();
82
+ const enqueueStateless = work => {
83
+ const result = statelessQueue.then(() => {
84
+ return work();
85
+ });
86
+ statelessQueue = result.catch(() => {});
87
+ return result;
88
+ };
89
+ const isLegacy = (ctx, body) => {
90
+ return (0, _modelcontextprotocol_server.classifyInboundRequest)({
91
+ httpMethod: ctx.method,
92
+ protocolVersionHeader: header(ctx, "mcp-protocol-version"),
93
+ mcpMethodHeader: header(ctx, "mcp-method"),
94
+ mcpNameHeader: header(ctx, "mcp-name"),
95
+ body
96
+ }).kind === "legacy";
97
+ };
98
+ return async (ctx, body) => {
99
+ if (!isLegacy(ctx, body)) {
100
+ await modernHandler(ctx.req, ctx.res, body);
101
+ return;
102
+ }
103
+ if (sharedTransport) {
104
+ await sharedTransport.handleRequest(ctx.req, ctx.res, body);
105
+ return;
106
+ }
107
+ await enqueueStateless(async () => {
108
+ const transport = new _modelcontextprotocol_node.NodeStreamableHTTPServerTransport({
109
+ sessionIdGenerator: void 0,
110
+ enableJsonResponse: true
111
+ });
112
+ try {
113
+ await server.connect(transport);
114
+ await transport.handleRequest(ctx.req, ctx.res, body);
115
+ } finally {
116
+ await transport.close();
117
+ }
118
+ });
119
+ };
120
+ };
121
+
27
122
  //#endregion
28
123
  //#region src/registerToolFromSchema.ts
29
- var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
30
- var patchedServerSet = /* @__PURE__ */new WeakSet();
124
+ /**
125
+ * Converts a plain JSON Schema into the Standard Schema `registerTool` accepts.
126
+ *
127
+ * A Standard Schema keeps advertisement and enforcement in separate fields:
128
+ * `~standard.jsonSchema` is what `tools/list` publishes, `~standard.validate`
129
+ * is what `tools/call` runs. Replacing only `validate` therefore keeps the
130
+ * schema fully visible to clients while leaving arguments unchecked.
131
+ */
132
+ var toStandardSchema = ({
133
+ inputSchema,
134
+ validateArguments
135
+ }) => {
136
+ const schema = (0, _modelcontextprotocol_server.fromJsonSchema)(inputSchema);
137
+ if (validateArguments) return schema;
138
+ return {
139
+ "~standard": {
140
+ ...schema["~standard"],
141
+ validate: value => {
142
+ return {
143
+ value
144
+ };
145
+ }
146
+ }
147
+ };
148
+ };
31
149
  /**
32
150
  * Registers a tool on an MCP server using a **plain JSON Schema** object for
33
151
  * `inputSchema` instead of a Zod shape.
34
152
  *
35
- * This is useful when tool definitions are shared between the MCP server and an
36
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
37
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
153
+ * This is useful when tool definitions are shared between the MCP server and
154
+ * an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
155
+ * a plain JSON Schema at runtime. Using this helper eliminates the lossy
38
156
  * JSON-Schema→Zod conversion that would otherwise be required.
39
157
  *
40
- * Internally the helper:
41
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
42
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
43
- * `tools/call` handler correctly routes requests and delivers raw args to
44
- * your handler.
45
- * 2. Stores the original JSON Schema and patches the `tools/list` response
46
- * handler so clients receive the verbatim JSON Schema over the wire.
158
+ * Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
159
+ * converts a JSON Schema into a Standard Schema that `registerTool` accepts
160
+ * directly, so the schema round-trips verbatim over `tools/list`. Arguments
161
+ * reach the handler unchecked unless `validateArguments` is enabled.
47
162
  *
48
163
  * @param server - The `McpServer` instance to register the tool on.
49
164
  * @param params - Tool configuration including name, description, inputSchema,
50
- * and handler.
165
+ * validateArguments, and handler.
51
166
  *
52
167
  * @example
53
168
  * ```typescript
@@ -77,38 +192,18 @@ var registerToolFromSchema = (server, params) => {
77
192
  type: "object",
78
193
  properties: {}
79
194
  },
195
+ validateArguments = false,
80
196
  handler
81
197
  } = params;
82
- if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
83
- rawSchemaRegistryMap.get(server).set(name, inputSchema);
84
198
  server.registerTool(name, {
85
199
  description,
86
- inputSchema: zod.z.record(zod.z.string(), zod.z.unknown())
200
+ inputSchema: toStandardSchema({
201
+ inputSchema,
202
+ validateArguments
203
+ })
87
204
  }, async args => {
88
205
  return handler(args);
89
206
  });
90
- if (!patchedServerSet.has(server)) {
91
- patchedServerSet.add(server);
92
- const rawServer = server.server;
93
- const origHandler = rawServer?._requestHandlers?.get("tools/list");
94
- if (!origHandler && process.env.NODE_ENV !== "production") console.warn("[registerToolFromSchema] Could not patch tools/list — internal MCP SDK structure may have changed. The tool will still be callable, but tools/list may return a Zod-derived schema instead of the verbatim JSON Schema.");
95
- if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
96
- const result = await origHandler(rawRequest, extra);
97
- const schemas = rawSchemaRegistryMap.get(server);
98
- if (!schemas) return result;
99
- return {
100
- ...result,
101
- tools: result.tools.map(tool => {
102
- const raw = schemas.get(tool.name);
103
- if (raw !== void 0) return {
104
- ...tool,
105
- inputSchema: raw
106
- };
107
- return tool;
108
- })
109
- };
110
- });
111
- }
112
207
  };
113
208
 
114
209
  //#endregion
@@ -427,24 +522,11 @@ var createMcpRouter = (server, options = {}) => {
427
522
  getApiHeaders,
428
523
  auth
429
524
  } = options;
430
- const isStateful = sessionIdGenerator !== void 0;
431
525
  const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
432
- let sharedTransport;
433
- if (isStateful) {
434
- sharedTransport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
435
- sessionIdGenerator,
436
- enableJsonResponse: true
437
- });
438
- server.connect(sharedTransport);
439
- }
440
- let statelessQueue = Promise.resolve();
441
- const enqueueStateless = work => {
442
- const result = statelessQueue.then(() => {
443
- return work();
444
- });
445
- statelessQueue = result.catch(() => {});
446
- return result;
447
- };
526
+ const serveRequest = createMcpRequestServer({
527
+ server,
528
+ sessionIdGenerator
529
+ });
448
530
  const router = new _ttoss_http_server.Router();
449
531
  let authCheck;
450
532
  if (auth) {
@@ -482,36 +564,16 @@ var createMcpRouter = (server, options = {}) => {
482
564
  const handleWithContext = async (ctx, body) => {
483
565
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
484
566
  const identity = ctx.state.user;
485
- const runRequest = async transport => {
486
- await transport.handleRequest(ctx.req, ctx.res, body);
567
+ ctx.req.auth = identity;
568
+ const runRequest = async () => {
569
+ await serveRequest(ctx, body);
487
570
  ctx.respond = false;
488
571
  };
489
- if (isStateful && sharedTransport) {
490
- if (needsContext) await requestContextStore.run({
491
- apiBaseUrl,
492
- apiHeaders,
493
- identity
494
- }, () => {
495
- return runRequest(sharedTransport);
496
- });else await runRequest(sharedTransport);
497
- } else await enqueueStateless(async () => {
498
- const transport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
499
- sessionIdGenerator: void 0,
500
- enableJsonResponse: true
501
- });
502
- try {
503
- await server.connect(transport);
504
- if (needsContext) await requestContextStore.run({
505
- apiBaseUrl,
506
- apiHeaders,
507
- identity
508
- }, () => {
509
- return runRequest(transport);
510
- });else await runRequest(transport);
511
- } finally {
512
- await transport.close();
513
- }
514
- });
572
+ if (needsContext) await requestContextStore.run({
573
+ apiBaseUrl,
574
+ apiHeaders,
575
+ identity
576
+ }, runRequest);else await runRequest();
515
577
  };
516
578
  const postHandler = async ctx => {
517
579
  try {
@@ -552,7 +614,7 @@ var createMcpRouter = (server, options = {}) => {
552
614
  Object.defineProperty(exports, 'McpServer', {
553
615
  enumerable: true,
554
616
  get: function () {
555
- return _modelcontextprotocol_sdk_server_mcp_js.McpServer;
617
+ return _modelcontextprotocol_server.McpServer;
556
618
  }
557
619
  });
558
620
  exports.apiCall = apiCall;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /// <reference types="node" />
3
- import { McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { CallToolResult, McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/server";
4
4
  import { Router } from "@ttoss/http-server";
5
5
  import accepts from "accepts";
6
6
  import { AsyncLocalStorage } from "async_hooks";
@@ -16,7 +16,6 @@ import compose from "koa-compose";
16
16
  import { ListenOptions, Socket } from "net";
17
17
  import { ParsedUrlQuery } from "querystring";
18
18
  import * as url from "url";
19
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
20
19
  import { z } from "zod";
21
20
 
22
21
  //#region ../../node_modules/.pnpm/@types+koa@3.0.3/node_modules/@types/koa/index.d.ts
@@ -704,10 +703,28 @@ interface RegisterToolFromSchemaParams {
704
703
  * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
705
704
  */
706
705
  inputSchema?: JsonObjectSchema;
706
+ /**
707
+ * Whether `tools/call` arguments are validated against `inputSchema` before
708
+ * the handler runs, rejecting a mismatch with an MCP error.
709
+ *
710
+ * Defaults to `false`, which forwards arguments to the handler unchecked —
711
+ * the behavior this helper has always had. `inputSchema` is still advertised
712
+ * verbatim over `tools/list` either way; this only controls enforcement.
713
+ *
714
+ * Enable it once you know `inputSchema` describes every value the tool
715
+ * genuinely accepts. Schemas generated from an OpenAPI document are a common
716
+ * source of *incomplete* ones — a field a client may send as `null` to clear
717
+ * it, or one accepting several shapes, is easy to emit as a bare
718
+ * `{ type: 'string' }`. Validating against a schema like that rejects calls
719
+ * the underlying API would have accepted.
720
+ *
721
+ * @default false
722
+ */
723
+ validateArguments?: boolean;
707
724
  /**
708
725
  * Tool handler invoked when the AI client calls the tool.
709
- * Receives the raw request arguments as a plain object (no Zod validation is
710
- * applied, so the shape matches whatever the client sends).
726
+ * Receives the request arguments, validated against `inputSchema` only when
727
+ * `validateArguments` is enabled.
711
728
  */
712
729
  handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
713
730
  }
@@ -715,22 +732,19 @@ interface RegisterToolFromSchemaParams {
715
732
  * Registers a tool on an MCP server using a **plain JSON Schema** object for
716
733
  * `inputSchema` instead of a Zod shape.
717
734
  *
718
- * This is useful when tool definitions are shared between the MCP server and an
719
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
720
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
735
+ * This is useful when tool definitions are shared between the MCP server and
736
+ * an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
737
+ * a plain JSON Schema at runtime. Using this helper eliminates the lossy
721
738
  * JSON-Schema→Zod conversion that would otherwise be required.
722
739
  *
723
- * Internally the helper:
724
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
725
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
726
- * `tools/call` handler correctly routes requests and delivers raw args to
727
- * your handler.
728
- * 2. Stores the original JSON Schema and patches the `tools/list` response
729
- * handler so clients receive the verbatim JSON Schema over the wire.
740
+ * Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
741
+ * converts a JSON Schema into a Standard Schema that `registerTool` accepts
742
+ * directly, so the schema round-trips verbatim over `tools/list`. Arguments
743
+ * reach the handler unchecked unless `validateArguments` is enabled.
730
744
  *
731
745
  * @param server - The `McpServer` instance to register the tool on.
732
746
  * @param params - Tool configuration including name, description, inputSchema,
733
- * and handler.
747
+ * validateArguments, and handler.
734
748
  *
735
749
  * @example
736
750
  * ```typescript
@@ -1024,6 +1038,10 @@ interface McpRouterOptions {
1024
1038
  * When provided, a single shared transport is created and sessions are tracked.
1025
1039
  * When undefined (default), the server operates in stateless mode where each
1026
1040
  * HTTP request uses its own transport instance.
1041
+ *
1042
+ * Applies to 2025-era traffic. The `2026-07-28` protocol revision has no
1043
+ * session concept in its core, so requests speaking that revision are always
1044
+ * served statelessly regardless of this option.
1027
1045
  */
1028
1046
  sessionIdGenerator?: () => string;
1029
1047
  /**
package/dist/index.d.mts CHANGED
@@ -2,8 +2,8 @@
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 } from "@modelcontextprotocol/server";
5
6
  import { z } from "zod";
6
- import { McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import accepts from "accepts";
8
8
  import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
9
9
  import Cookies from "cookies";
@@ -18,7 +18,6 @@ import compose from "koa-compose";
18
18
  import { ListenOptions, Socket } from "net";
19
19
  import { ParsedUrlQuery } from "querystring";
20
20
  import * as url from "url";
21
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
21
 
23
22
  //#region ../../node_modules/.pnpm/@types+koa@3.0.3/node_modules/@types/koa/index.d.ts
24
23
  declare interface ContextDelegatedRequest {
@@ -705,10 +704,28 @@ interface RegisterToolFromSchemaParams {
705
704
  * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
706
705
  */
707
706
  inputSchema?: JsonObjectSchema;
707
+ /**
708
+ * Whether `tools/call` arguments are validated against `inputSchema` before
709
+ * the handler runs, rejecting a mismatch with an MCP error.
710
+ *
711
+ * Defaults to `false`, which forwards arguments to the handler unchecked —
712
+ * the behavior this helper has always had. `inputSchema` is still advertised
713
+ * verbatim over `tools/list` either way; this only controls enforcement.
714
+ *
715
+ * Enable it once you know `inputSchema` describes every value the tool
716
+ * genuinely accepts. Schemas generated from an OpenAPI document are a common
717
+ * source of *incomplete* ones — a field a client may send as `null` to clear
718
+ * it, or one accepting several shapes, is easy to emit as a bare
719
+ * `{ type: 'string' }`. Validating against a schema like that rejects calls
720
+ * the underlying API would have accepted.
721
+ *
722
+ * @default false
723
+ */
724
+ validateArguments?: boolean;
708
725
  /**
709
726
  * Tool handler invoked when the AI client calls the tool.
710
- * Receives the raw request arguments as a plain object (no Zod validation is
711
- * applied, so the shape matches whatever the client sends).
727
+ * Receives the request arguments, validated against `inputSchema` only when
728
+ * `validateArguments` is enabled.
712
729
  */
713
730
  handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
714
731
  }
@@ -716,22 +733,19 @@ interface RegisterToolFromSchemaParams {
716
733
  * Registers a tool on an MCP server using a **plain JSON Schema** object for
717
734
  * `inputSchema` instead of a Zod shape.
718
735
  *
719
- * This is useful when tool definitions are shared between the MCP server and an
720
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
721
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
736
+ * This is useful when tool definitions are shared between the MCP server and
737
+ * an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
738
+ * a plain JSON Schema at runtime. Using this helper eliminates the lossy
722
739
  * JSON-Schema→Zod conversion that would otherwise be required.
723
740
  *
724
- * Internally the helper:
725
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
726
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
727
- * `tools/call` handler correctly routes requests and delivers raw args to
728
- * your handler.
729
- * 2. Stores the original JSON Schema and patches the `tools/list` response
730
- * handler so clients receive the verbatim JSON Schema over the wire.
741
+ * Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
742
+ * converts a JSON Schema into a Standard Schema that `registerTool` accepts
743
+ * directly, so the schema round-trips verbatim over `tools/list`. Arguments
744
+ * reach the handler unchecked unless `validateArguments` is enabled.
731
745
  *
732
746
  * @param server - The `McpServer` instance to register the tool on.
733
747
  * @param params - Tool configuration including name, description, inputSchema,
734
- * and handler.
748
+ * validateArguments, and handler.
735
749
  *
736
750
  * @example
737
751
  * ```typescript
@@ -1025,6 +1039,10 @@ interface McpRouterOptions {
1025
1039
  * When provided, a single shared transport is created and sessions are tracked.
1026
1040
  * When undefined (default), the server operates in stateless mode where each
1027
1041
  * HTTP request uses its own transport instance.
1042
+ *
1043
+ * Applies to 2025-era traffic. The `2026-07-28` protocol revision has no
1044
+ * session concept in its core, so requests speaking that revision are always
1045
+ * served statelessly regardless of this option.
1028
1046
  */
1029
1047
  sessionIdGenerator?: () => string;
1030
1048
  /**
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
2
  import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
4
3
  import { Router } from "@ttoss/http-server";
5
4
  import { authMiddleware } from "@ttoss/http-server-auth";
6
5
  import { AsyncLocalStorage } from "node:async_hooks";
7
- import { z, z as z$1 } from "zod";
8
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { NodeStreamableHTTPServerTransport, toNodeHandler } from "@modelcontextprotocol/node";
7
+ import { McpServer, classifyInboundRequest, createMcpHandler, fromJsonSchema } from "@modelcontextprotocol/server";
8
+ import { z } from "zod";
9
9
 
10
10
  //#region src/context.ts
11
11
  var requestContextStore = new AsyncLocalStorage();
@@ -21,30 +21,145 @@ var getIdentity = () => {
21
21
  return requestContextStore.getStore()?.identity;
22
22
  };
23
23
 
24
+ //#endregion
25
+ //#region src/serveMcpRequest.ts
26
+ /**
27
+ * Reads a header as `string | undefined`. Koa's `ctx.get` returns `''` for a
28
+ * missing header, which the classifier would read as "header present but
29
+ * empty" rather than absent.
30
+ */
31
+ var header = (ctx, name) => {
32
+ const value = ctx.get(name);
33
+ return value === "" ? void 0 : value;
34
+ };
35
+ /**
36
+ * Builds the per-request serving function used by `createMcpRouter`, routing
37
+ * each request to the protocol revision it actually speaks.
38
+ *
39
+ * Requests are classified once at the boundary with the SDK's own
40
+ * `classifyInboundRequest`:
41
+ *
42
+ * - **2025-era traffic** (everything without a `2026-07-28` per-request
43
+ * envelope — which is all traffic from today's MCP clients) is served over
44
+ * `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, the
45
+ * same wiring and the same plain-JSON responses this package has always
46
+ * produced.
47
+ * - **`2026-07-28` traffic** is served by `createMcpHandler` with
48
+ * `legacy: 'reject'`, so the modern revision is handled by the SDK's
49
+ * stateless core rather than the transport above. Malformed requests are
50
+ * routed here too, so the SDK emits its own spec-defined rejection instead of
51
+ * this package inventing one.
52
+ *
53
+ * Splitting on the classifier — instead of letting `createMcpHandler` serve
54
+ * both eras through its built-in legacy fallback — is what keeps 2025-era
55
+ * responses byte-compatible. That fallback constructs its transport with only
56
+ * `sessionIdGenerator: undefined` and no way to pass `enableJsonResponse`, so
57
+ * every response it produces is SSE-framed (`text/event-stream`) regardless of
58
+ * the handler's `responseMode`. Owning the legacy branch keeps `application/json`.
59
+ */
60
+ var createMcpRequestServer = ({
61
+ server,
62
+ sessionIdGenerator
63
+ }) => {
64
+ const isStateful = sessionIdGenerator !== void 0;
65
+ const modernHandler = toNodeHandler(createMcpHandler(() => {
66
+ return server;
67
+ }, {
68
+ legacy: "reject"
69
+ }));
70
+ let sharedTransport;
71
+ if (isStateful) {
72
+ sharedTransport = new NodeStreamableHTTPServerTransport({
73
+ sessionIdGenerator,
74
+ enableJsonResponse: true
75
+ });
76
+ server.connect(sharedTransport);
77
+ }
78
+ let statelessQueue = Promise.resolve();
79
+ const enqueueStateless = work => {
80
+ const result = statelessQueue.then(() => {
81
+ return work();
82
+ });
83
+ statelessQueue = result.catch(() => {});
84
+ return result;
85
+ };
86
+ const isLegacy = (ctx, body) => {
87
+ return classifyInboundRequest({
88
+ httpMethod: ctx.method,
89
+ protocolVersionHeader: header(ctx, "mcp-protocol-version"),
90
+ mcpMethodHeader: header(ctx, "mcp-method"),
91
+ mcpNameHeader: header(ctx, "mcp-name"),
92
+ body
93
+ }).kind === "legacy";
94
+ };
95
+ return async (ctx, body) => {
96
+ if (!isLegacy(ctx, body)) {
97
+ await modernHandler(ctx.req, ctx.res, body);
98
+ return;
99
+ }
100
+ if (sharedTransport) {
101
+ await sharedTransport.handleRequest(ctx.req, ctx.res, body);
102
+ return;
103
+ }
104
+ await enqueueStateless(async () => {
105
+ const transport = new NodeStreamableHTTPServerTransport({
106
+ sessionIdGenerator: void 0,
107
+ enableJsonResponse: true
108
+ });
109
+ try {
110
+ await server.connect(transport);
111
+ await transport.handleRequest(ctx.req, ctx.res, body);
112
+ } finally {
113
+ await transport.close();
114
+ }
115
+ });
116
+ };
117
+ };
118
+
24
119
  //#endregion
25
120
  //#region src/registerToolFromSchema.ts
26
- var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
27
- var patchedServerSet = /* @__PURE__ */new WeakSet();
121
+ /**
122
+ * Converts a plain JSON Schema into the Standard Schema `registerTool` accepts.
123
+ *
124
+ * A Standard Schema keeps advertisement and enforcement in separate fields:
125
+ * `~standard.jsonSchema` is what `tools/list` publishes, `~standard.validate`
126
+ * is what `tools/call` runs. Replacing only `validate` therefore keeps the
127
+ * schema fully visible to clients while leaving arguments unchecked.
128
+ */
129
+ var toStandardSchema = ({
130
+ inputSchema,
131
+ validateArguments
132
+ }) => {
133
+ const schema = fromJsonSchema(inputSchema);
134
+ if (validateArguments) return schema;
135
+ return {
136
+ "~standard": {
137
+ ...schema["~standard"],
138
+ validate: value => {
139
+ return {
140
+ value
141
+ };
142
+ }
143
+ }
144
+ };
145
+ };
28
146
  /**
29
147
  * Registers a tool on an MCP server using a **plain JSON Schema** object for
30
148
  * `inputSchema` instead of a Zod shape.
31
149
  *
32
- * This is useful when tool definitions are shared between the MCP server and an
33
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
34
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
150
+ * This is useful when tool definitions are shared between the MCP server and
151
+ * an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
152
+ * a plain JSON Schema at runtime. Using this helper eliminates the lossy
35
153
  * JSON-Schema→Zod conversion that would otherwise be required.
36
154
  *
37
- * Internally the helper:
38
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
39
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
40
- * `tools/call` handler correctly routes requests and delivers raw args to
41
- * your handler.
42
- * 2. Stores the original JSON Schema and patches the `tools/list` response
43
- * handler so clients receive the verbatim JSON Schema over the wire.
155
+ * Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
156
+ * converts a JSON Schema into a Standard Schema that `registerTool` accepts
157
+ * directly, so the schema round-trips verbatim over `tools/list`. Arguments
158
+ * reach the handler unchecked unless `validateArguments` is enabled.
44
159
  *
45
160
  * @param server - The `McpServer` instance to register the tool on.
46
161
  * @param params - Tool configuration including name, description, inputSchema,
47
- * and handler.
162
+ * validateArguments, and handler.
48
163
  *
49
164
  * @example
50
165
  * ```typescript
@@ -74,38 +189,18 @@ var registerToolFromSchema = (server, params) => {
74
189
  type: "object",
75
190
  properties: {}
76
191
  },
192
+ validateArguments = false,
77
193
  handler
78
194
  } = params;
79
- if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
80
- rawSchemaRegistryMap.get(server).set(name, inputSchema);
81
195
  server.registerTool(name, {
82
196
  description,
83
- inputSchema: z$1.record(z$1.string(), z$1.unknown())
197
+ inputSchema: toStandardSchema({
198
+ inputSchema,
199
+ validateArguments
200
+ })
84
201
  }, async args => {
85
202
  return handler(args);
86
203
  });
87
- if (!patchedServerSet.has(server)) {
88
- patchedServerSet.add(server);
89
- const rawServer = server.server;
90
- const origHandler = rawServer?._requestHandlers?.get("tools/list");
91
- if (!origHandler && process.env.NODE_ENV !== "production") console.warn("[registerToolFromSchema] Could not patch tools/list — internal MCP SDK structure may have changed. The tool will still be callable, but tools/list may return a Zod-derived schema instead of the verbatim JSON Schema.");
92
- if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
93
- const result = await origHandler(rawRequest, extra);
94
- const schemas = rawSchemaRegistryMap.get(server);
95
- if (!schemas) return result;
96
- return {
97
- ...result,
98
- tools: result.tools.map(tool => {
99
- const raw = schemas.get(tool.name);
100
- if (raw !== void 0) return {
101
- ...tool,
102
- inputSchema: raw
103
- };
104
- return tool;
105
- })
106
- };
107
- });
108
- }
109
204
  };
110
205
 
111
206
  //#endregion
@@ -424,24 +519,11 @@ var createMcpRouter = (server, options = {}) => {
424
519
  getApiHeaders,
425
520
  auth
426
521
  } = options;
427
- const isStateful = sessionIdGenerator !== void 0;
428
522
  const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
429
- let sharedTransport;
430
- if (isStateful) {
431
- sharedTransport = new StreamableHTTPServerTransport({
432
- sessionIdGenerator,
433
- enableJsonResponse: true
434
- });
435
- server.connect(sharedTransport);
436
- }
437
- let statelessQueue = Promise.resolve();
438
- const enqueueStateless = work => {
439
- const result = statelessQueue.then(() => {
440
- return work();
441
- });
442
- statelessQueue = result.catch(() => {});
443
- return result;
444
- };
523
+ const serveRequest = createMcpRequestServer({
524
+ server,
525
+ sessionIdGenerator
526
+ });
445
527
  const router = new Router();
446
528
  let authCheck;
447
529
  if (auth) {
@@ -479,36 +561,16 @@ var createMcpRouter = (server, options = {}) => {
479
561
  const handleWithContext = async (ctx, body) => {
480
562
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
481
563
  const identity = ctx.state.user;
482
- const runRequest = async transport => {
483
- await transport.handleRequest(ctx.req, ctx.res, body);
564
+ ctx.req.auth = identity;
565
+ const runRequest = async () => {
566
+ await serveRequest(ctx, body);
484
567
  ctx.respond = false;
485
568
  };
486
- if (isStateful && sharedTransport) {
487
- if (needsContext) await requestContextStore.run({
488
- apiBaseUrl,
489
- apiHeaders,
490
- identity
491
- }, () => {
492
- return runRequest(sharedTransport);
493
- });else await runRequest(sharedTransport);
494
- } else await enqueueStateless(async () => {
495
- const transport = new StreamableHTTPServerTransport({
496
- sessionIdGenerator: void 0,
497
- enableJsonResponse: true
498
- });
499
- try {
500
- await server.connect(transport);
501
- if (needsContext) await requestContextStore.run({
502
- apiBaseUrl,
503
- apiHeaders,
504
- identity
505
- }, () => {
506
- return runRequest(transport);
507
- });else await runRequest(transport);
508
- } finally {
509
- await transport.close();
510
- }
511
- });
569
+ if (needsContext) await requestContextStore.run({
570
+ apiBaseUrl,
571
+ apiHeaders,
572
+ identity
573
+ }, runRequest);else await runRequest();
512
574
  };
513
575
  const postHandler = async ctx => {
514
576
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.23.1",
3
+ "version": "0.24.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,11 +33,12 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.29.0",
36
+ "@modelcontextprotocol/node": "^2.0.0",
37
+ "@modelcontextprotocol/server": "^2.0.0",
37
38
  "zod": "^4.4.3",
38
- "@ttoss/auth-core": "^0.12.0",
39
- "@ttoss/http-server": "^0.7.1",
40
- "@ttoss/http-server-auth": "^0.5.0"
39
+ "@ttoss/auth-core": "^0.13.0",
40
+ "@ttoss/http-server-auth": "^0.6.0",
41
+ "@ttoss/http-server": "^0.7.1"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/koa": "^3.0.3",
@@ -46,9 +47,6 @@
46
47
  "tsdown": "^0.22.2",
47
48
  "@ttoss/config": "^1.37.17"
48
49
  },
49
- "peerDependencies": {
50
- "@modelcontextprotocol/sdk": "^1.29.0"
51
- },
52
50
  "publishConfig": {
53
51
  "access": "public",
54
52
  "provenance": true