@ttoss/http-server-mcp 0.22.0 → 0.23.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
@@ -266,6 +266,41 @@ mcpServer.registerTool(
266
266
 
267
267
  Cognito encodes scopes as a space-separated string in `payload.scope` (e.g. `"openid mcp:access admin"`).
268
268
 
269
+ ### Resource indicator validation (RFC 8707)
270
+
271
+ `requiredScopes` checks _what_ a token can do; `resourceIndicator` checks that the token was minted _for this server_ in the first place. Without it, a token issued by your authorization server for a different resource (another API, another MCP server) would still pass verification here as long as the signature checks out — the classic confused-deputy risk RFC 8707 exists to close.
272
+
273
+ ```typescript
274
+ createMcpRouter(mcpServer, {
275
+ auth: {
276
+ cognitoUserPool: { userPoolId: '...', clientId: '...' },
277
+ resourceIndicator: 'https://mcp.example.com',
278
+ },
279
+ });
280
+ ```
281
+
282
+ The verified token's `aud` claim must include at least one of the configured values (a single string or an array of strings, for servers reachable under multiple hostnames), or the request receives `401 Unauthorized`. This applies uniformly regardless of the verification method — `cognitoUserPool`, a custom `verifyToken`, or `@ttoss/auth-core/oidc`'s `createOidcVerifier` below.
283
+
284
+ ### OIDC providers (Entra ID, Okta, Auth0, …)
285
+
286
+ For any standards-compliant OIDC provider beyond Cognito, use `createOidcVerifier` from `@ttoss/auth-core/oidc`. It discovers the provider's signing keys from its `/.well-known/openid-configuration` document — no manual JWKS URL, no hand-rolled discovery:
287
+
288
+ ```typescript
289
+ import { createOidcVerifier } from '@ttoss/auth-core/oidc';
290
+ import { createMcpRouter } from '@ttoss/http-server-mcp';
291
+
292
+ const mcpRouter = createMcpRouter(mcpServer, {
293
+ auth: {
294
+ verifyToken: createOidcVerifier({
295
+ issuer: 'https://login.microsoftonline.com/<tenant>/v2.0',
296
+ }),
297
+ resourceIndicator: 'https://mcp.example.com',
298
+ },
299
+ });
300
+ ```
301
+
302
+ `createOidcVerifier` deliberately does not validate `aud` itself — pass `resourceIndicator` alongside it, as shown, so audience validation stays consistent across every auth method this router supports. See the [`@ttoss/auth-core` README](https://ttoss.dev/docs/modules/packages/auth-core) for details on key caching and rotation.
303
+
269
304
  ### OAuth Protected Resource Metadata
270
305
 
271
306
  MCP clients (Claude, Cursor, etc.) fetch `/.well-known/oauth-protected-resource` to discover which authorization server issues tokens for your MCP server. The endpoint must be **unauthenticated** — MCP clients call it before they have a token.
@@ -460,6 +495,7 @@ Creates a Koa router configured to handle MCP protocol requests.
460
495
  - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`; the metadata `resource` is set to `resourceServerUrl + path` so clients following `resource` land on the actual MCP endpoint
461
496
  - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize', 'tools/list']`)
462
497
  - `auth.resourceMetadataUrl` — Emit RFC 9728 `WWW-Authenticate: Bearer resource_metadata="…"` on 401
498
+ - `auth.resourceIndicator` — Expected `aud` value(s) (RFC 8707); rejects tokens minted for a different resource. See [Resource indicator validation](#resource-indicator-validation-rfc-8707)
463
499
 
464
500
  **Returns:** `Router` — Koa router instance
465
501
 
package/dist/index.cjs CHANGED
@@ -6,8 +6,8 @@ let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextp
6
6
  let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
7
7
  let _ttoss_http_server = require("@ttoss/http-server");
8
8
  let _ttoss_http_server_auth = require("@ttoss/http-server-auth");
9
- let zod = require("zod");
10
9
  let node_async_hooks = require("node:async_hooks");
10
+ let zod = require("zod");
11
11
  let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
12
12
 
13
13
  //#region src/context.ts
@@ -24,6 +24,93 @@ var getIdentity = () => {
24
24
  return requestContextStore.getStore()?.identity;
25
25
  };
26
26
 
27
+ //#endregion
28
+ //#region src/registerToolFromSchema.ts
29
+ var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
30
+ var patchedServerSet = /* @__PURE__ */new WeakSet();
31
+ /**
32
+ * Registers a tool on an MCP server using a **plain JSON Schema** object for
33
+ * `inputSchema` instead of a Zod shape.
34
+ *
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
38
+ * JSON-Schema→Zod conversion that would otherwise be required.
39
+ *
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.
47
+ *
48
+ * @param server - The `McpServer` instance to register the tool on.
49
+ * @param params - Tool configuration including name, description, inputSchema,
50
+ * and handler.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
55
+ *
56
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
57
+ *
58
+ * registerToolFromSchema(server, {
59
+ * name: 'get-project',
60
+ * description: 'Get a project by ID',
61
+ * inputSchema: {
62
+ * type: 'object',
63
+ * properties: { id: { type: 'string', description: 'Project public ID' } },
64
+ * required: ['id'],
65
+ * },
66
+ * handler: async ({ id }) => ({
67
+ * content: [{ type: 'text', text: `Project: ${id}` }],
68
+ * }),
69
+ * });
70
+ * ```
71
+ */
72
+ var registerToolFromSchema = (server, params) => {
73
+ const {
74
+ name,
75
+ description,
76
+ inputSchema = {
77
+ type: "object",
78
+ properties: {}
79
+ },
80
+ handler
81
+ } = params;
82
+ if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
83
+ rawSchemaRegistryMap.get(server).set(name, inputSchema);
84
+ server.registerTool(name, {
85
+ description,
86
+ inputSchema: zod.z.record(zod.z.string(), zod.z.unknown())
87
+ }, async args => {
88
+ return handler(args);
89
+ });
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
+ };
113
+
27
114
  //#endregion
28
115
  //#region src/createGatedToolRegistrar.ts
29
116
  var toolError = message => {
@@ -145,19 +232,39 @@ var createGatedToolRegistrar = ({
145
232
  //#region src/index.ts
146
233
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
147
234
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
235
+ /**
236
+ * Asserts that a verified token's `aud` claim includes at least one of the
237
+ * expected resource indicator values. `aud` may be a single string or an
238
+ * array of strings per the JWT spec (RFC 7519); both forms are checked.
239
+ */
240
+ var assertResourceIndicator = (payload, resourceIndicator) => {
241
+ const expected = Array.isArray(resourceIndicator) ? resourceIndicator : [resourceIndicator];
242
+ const aud = payload?.aud;
243
+ if (!(typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : []).some(audience => {
244
+ return typeof audience === "string" && expected.includes(audience);
245
+ })) throw new Error(`Token audience does not include the expected resource indicator (expected one of: ${expected.join(", ")}).`);
246
+ };
148
247
  /** Builds the token verifier from the MCP auth options. */
149
248
  var buildVerifyToken = auth => {
150
- if (auth.cognitoUserPool) {
151
- const verifier = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
152
- tokenUse: "access",
153
- ...auth.cognitoUserPool
154
- });
155
- return token => {
156
- return verifier.verify(token);
157
- };
158
- }
159
- if (auth.verifyToken) return auth.verifyToken;
160
- throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
249
+ const verify = (() => {
250
+ if (auth.cognitoUserPool) {
251
+ const verifier = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
252
+ tokenUse: "access",
253
+ ...auth.cognitoUserPool
254
+ });
255
+ return token => {
256
+ return verifier.verify(token);
257
+ };
258
+ }
259
+ if (auth.verifyToken) return auth.verifyToken;
260
+ throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
261
+ })();
262
+ if (!auth.resourceIndicator) return verify;
263
+ return async token => {
264
+ const payload = await verify(token);
265
+ assertResourceIndicator(payload, auth.resourceIndicator);
266
+ return payload;
267
+ };
161
268
  };
162
269
  /**
163
270
  * Generic HTTP helper for use inside MCP tool handlers.
@@ -440,90 +547,6 @@ var createMcpRouter = (server, options = {}) => {
440
547
  }
441
548
  return router;
442
549
  };
443
- var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
444
- var patchedServerSet = /* @__PURE__ */new WeakSet();
445
- /**
446
- * Registers a tool on an MCP server using a **plain JSON Schema** object for
447
- * `inputSchema` instead of a Zod shape.
448
- *
449
- * This is useful when tool definitions are shared between the MCP server and an
450
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
451
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
452
- * JSON-Schema→Zod conversion that would otherwise be required.
453
- *
454
- * Internally the helper:
455
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
456
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
457
- * `tools/call` handler correctly routes requests and delivers raw args to
458
- * your handler.
459
- * 2. Stores the original JSON Schema and patches the `tools/list` response
460
- * handler so clients receive the verbatim JSON Schema over the wire.
461
- *
462
- * @param server - The `McpServer` instance to register the tool on.
463
- * @param params - Tool configuration including name, description, inputSchema,
464
- * and handler.
465
- *
466
- * @example
467
- * ```typescript
468
- * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
469
- *
470
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
471
- *
472
- * registerToolFromSchema(server, {
473
- * name: 'get-project',
474
- * description: 'Get a project by ID',
475
- * inputSchema: {
476
- * type: 'object',
477
- * properties: { id: { type: 'string', description: 'Project public ID' } },
478
- * required: ['id'],
479
- * },
480
- * handler: async ({ id }) => ({
481
- * content: [{ type: 'text', text: `Project: ${id}` }],
482
- * }),
483
- * });
484
- * ```
485
- */
486
- var registerToolFromSchema = (server, params) => {
487
- const {
488
- name,
489
- description,
490
- inputSchema = {
491
- type: "object",
492
- properties: {}
493
- },
494
- handler
495
- } = params;
496
- if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
497
- rawSchemaRegistryMap.get(server).set(name, inputSchema);
498
- server.registerTool(name, {
499
- description,
500
- inputSchema: zod.z.record(zod.z.string(), zod.z.unknown())
501
- }, async args => {
502
- return handler(args);
503
- });
504
- if (!patchedServerSet.has(server)) {
505
- patchedServerSet.add(server);
506
- const rawServer = server.server;
507
- const origHandler = rawServer?._requestHandlers?.get("tools/list");
508
- 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.");
509
- if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
510
- const result = await origHandler(rawRequest, extra);
511
- const schemas = rawSchemaRegistryMap.get(server);
512
- if (!schemas) return result;
513
- return {
514
- ...result,
515
- tools: result.tools.map(tool => {
516
- const raw = schemas.get(tool.name);
517
- if (raw !== void 0) return {
518
- ...tool,
519
- inputSchema: raw
520
- };
521
- return tool;
522
- })
523
- };
524
- });
525
- }
526
- };
527
550
 
528
551
  //#endregion
529
552
  Object.defineProperty(exports, 'McpServer', {
package/dist/index.d.cts CHANGED
@@ -1,7 +1,6 @@
1
1
 
2
2
  /// <reference types="node" />
3
3
  import { McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
4
  import { Router } from "@ttoss/http-server";
6
5
  import accepts from "accepts";
7
6
  import { AsyncLocalStorage } from "async_hooks";
@@ -17,6 +16,7 @@ import compose from "koa-compose";
17
16
  import { ListenOptions, Socket } from "net";
18
17
  import { ParsedUrlQuery } from "querystring";
19
18
  import * as url from "url";
19
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
20
20
  import { z } from "zod";
21
21
 
22
22
  //#region ../../node_modules/.pnpm/@types+koa@3.0.3/node_modules/@types/koa/index.d.ts
@@ -676,6 +676,84 @@ declare namespace Application {
676
676
  */
677
677
  declare const getIdentity: <T = unknown>() => T | undefined;
678
678
  //#endregion
679
+ //#region src/registerToolFromSchema.d.ts
680
+ /**
681
+ * A plain JSON Schema object (draft-07 compatible) describing the shape of a
682
+ * tool's input. Used with {@link registerToolFromSchema} as an alternative to
683
+ * providing a Zod shape, enabling lossless round-trips for schemas that contain
684
+ * features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
685
+ */
686
+ interface JsonObjectSchema {
687
+ type: 'object';
688
+ properties?: Record<string, unknown>;
689
+ required?: string[];
690
+ [key: string]: unknown;
691
+ }
692
+ /**
693
+ * Parameters accepted by {@link registerToolFromSchema}.
694
+ */
695
+ interface RegisterToolFromSchemaParams {
696
+ /** Unique tool name. */
697
+ name: string;
698
+ /** Human-readable description shown to the AI client. */
699
+ description?: string;
700
+ /**
701
+ * Plain JSON Schema that describes the tool's input object.
702
+ * This schema is forwarded verbatim over the MCP wire protocol, so any
703
+ * JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
704
+ * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
705
+ */
706
+ inputSchema?: JsonObjectSchema;
707
+ /**
708
+ * 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).
711
+ */
712
+ handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
713
+ }
714
+ /**
715
+ * Registers a tool on an MCP server using a **plain JSON Schema** object for
716
+ * `inputSchema` instead of a Zod shape.
717
+ *
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
721
+ * JSON-Schema→Zod conversion that would otherwise be required.
722
+ *
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.
730
+ *
731
+ * @param server - The `McpServer` instance to register the tool on.
732
+ * @param params - Tool configuration including name, description, inputSchema,
733
+ * and handler.
734
+ *
735
+ * @example
736
+ * ```typescript
737
+ * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
738
+ *
739
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
740
+ *
741
+ * registerToolFromSchema(server, {
742
+ * name: 'get-project',
743
+ * description: 'Get a project by ID',
744
+ * inputSchema: {
745
+ * type: 'object',
746
+ * properties: { id: { type: 'string', description: 'Project public ID' } },
747
+ * required: ['id'],
748
+ * },
749
+ * handler: async ({ id }) => ({
750
+ * content: [{ type: 'text', text: `Project: ${id}` }],
751
+ * }),
752
+ * });
753
+ * ```
754
+ */
755
+ declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
756
+ //#endregion
679
757
  //#region src/createGatedToolRegistrar.d.ts
680
758
  /** Resolved identity for a gated tool call. */
681
759
  type ToolIdentity = {
@@ -816,6 +894,22 @@ interface McpAuthOptions {
816
894
  resourceServerUrl?: string;
817
895
  /** URL of the OAuth Authorization Server that issues tokens for this resource. */
818
896
  authorizationServerUrl?: string;
897
+ /**
898
+ * Expected audience — the resource indicator (RFC 8707) this MCP server
899
+ * identifies as. When set, the verified token's `aud` claim must include at
900
+ * least one of these values, or the request is rejected with `401`. Without
901
+ * this check, a token minted for a *different* resource but signed by the
902
+ * same authorization server would still be accepted here — the classic
903
+ * confused-deputy risk RFC 8707 exists to close.
904
+ *
905
+ * Applies uniformly regardless of whether verification is done via
906
+ * `cognitoUserPool`, a custom `verifyToken`, or `@ttoss/auth-core/oidc`'s
907
+ * `createOidcVerifier` (which intentionally leaves audience validation to
908
+ * the caller for this reason).
909
+ *
910
+ * @example 'https://mcp.example.com'
911
+ */
912
+ resourceIndicator?: string | string[];
819
913
  }
820
914
  /**
821
915
  * Options for a single `apiCall` request.
@@ -1039,81 +1133,5 @@ interface McpRouterOptions {
1039
1133
  * ```
1040
1134
  */
1041
1135
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => Router<Application.DefaultState, Application.DefaultContext>;
1042
- /**
1043
- * A plain JSON Schema object (draft-07 compatible) describing the shape of a
1044
- * tool's input. Used with {@link registerToolFromSchema} as an alternative to
1045
- * providing a Zod shape, enabling lossless round-trips for schemas that contain
1046
- * features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
1047
- */
1048
- interface JsonObjectSchema {
1049
- type: 'object';
1050
- properties?: Record<string, unknown>;
1051
- required?: string[];
1052
- [key: string]: unknown;
1053
- }
1054
- /**
1055
- * Parameters accepted by {@link registerToolFromSchema}.
1056
- */
1057
- interface RegisterToolFromSchemaParams {
1058
- /** Unique tool name. */
1059
- name: string;
1060
- /** Human-readable description shown to the AI client. */
1061
- description?: string;
1062
- /**
1063
- * Plain JSON Schema that describes the tool's input object.
1064
- * This schema is forwarded verbatim over the MCP wire protocol, so any
1065
- * JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
1066
- * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
1067
- */
1068
- inputSchema?: JsonObjectSchema;
1069
- /**
1070
- * Tool handler invoked when the AI client calls the tool.
1071
- * Receives the raw request arguments as a plain object (no Zod validation is
1072
- * applied, so the shape matches whatever the client sends).
1073
- */
1074
- handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
1075
- }
1076
- /**
1077
- * Registers a tool on an MCP server using a **plain JSON Schema** object for
1078
- * `inputSchema` instead of a Zod shape.
1079
- *
1080
- * This is useful when tool definitions are shared between the MCP server and an
1081
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
1082
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
1083
- * JSON-Schema→Zod conversion that would otherwise be required.
1084
- *
1085
- * Internally the helper:
1086
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
1087
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
1088
- * `tools/call` handler correctly routes requests and delivers raw args to
1089
- * your handler.
1090
- * 2. Stores the original JSON Schema and patches the `tools/list` response
1091
- * handler so clients receive the verbatim JSON Schema over the wire.
1092
- *
1093
- * @param server - The `McpServer` instance to register the tool on.
1094
- * @param params - Tool configuration including name, description, inputSchema,
1095
- * and handler.
1096
- *
1097
- * @example
1098
- * ```typescript
1099
- * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
1100
- *
1101
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
1102
- *
1103
- * registerToolFromSchema(server, {
1104
- * name: 'get-project',
1105
- * description: 'Get a project by ID',
1106
- * inputSchema: {
1107
- * type: 'object',
1108
- * properties: { id: { type: 'string', description: 'Project public ID' } },
1109
- * required: ['id'],
1110
- * },
1111
- * handler: async ({ id }) => ({
1112
- * content: [{ type: 'text', text: `Project: ${id}` }],
1113
- * }),
1114
- * });
1115
- * ```
1116
- */
1117
- declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1118
1136
  //#endregion
1119
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
1137
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,9 @@
1
1
 
2
2
  /// <reference types="node" />
3
3
  import { Router } from "@ttoss/http-server";
4
- import { z } from "zod";
5
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { z } from "zod";
6
6
  import { McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8
7
  import accepts from "accepts";
9
8
  import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
10
9
  import Cookies from "cookies";
@@ -19,6 +18,7 @@ import compose from "koa-compose";
19
18
  import { ListenOptions, Socket } from "net";
20
19
  import { ParsedUrlQuery } from "querystring";
21
20
  import * as url from "url";
21
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
22
 
23
23
  //#region ../../node_modules/.pnpm/@types+koa@3.0.3/node_modules/@types/koa/index.d.ts
24
24
  declare interface ContextDelegatedRequest {
@@ -677,6 +677,84 @@ declare namespace Application {
677
677
  */
678
678
  declare const getIdentity: <T = unknown>() => T | undefined;
679
679
  //#endregion
680
+ //#region src/registerToolFromSchema.d.ts
681
+ /**
682
+ * A plain JSON Schema object (draft-07 compatible) describing the shape of a
683
+ * tool's input. Used with {@link registerToolFromSchema} as an alternative to
684
+ * providing a Zod shape, enabling lossless round-trips for schemas that contain
685
+ * features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
686
+ */
687
+ interface JsonObjectSchema {
688
+ type: 'object';
689
+ properties?: Record<string, unknown>;
690
+ required?: string[];
691
+ [key: string]: unknown;
692
+ }
693
+ /**
694
+ * Parameters accepted by {@link registerToolFromSchema}.
695
+ */
696
+ interface RegisterToolFromSchemaParams {
697
+ /** Unique tool name. */
698
+ name: string;
699
+ /** Human-readable description shown to the AI client. */
700
+ description?: string;
701
+ /**
702
+ * Plain JSON Schema that describes the tool's input object.
703
+ * This schema is forwarded verbatim over the MCP wire protocol, so any
704
+ * JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
705
+ * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
706
+ */
707
+ inputSchema?: JsonObjectSchema;
708
+ /**
709
+ * 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).
712
+ */
713
+ handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
714
+ }
715
+ /**
716
+ * Registers a tool on an MCP server using a **plain JSON Schema** object for
717
+ * `inputSchema` instead of a Zod shape.
718
+ *
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
722
+ * JSON-Schema→Zod conversion that would otherwise be required.
723
+ *
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.
731
+ *
732
+ * @param server - The `McpServer` instance to register the tool on.
733
+ * @param params - Tool configuration including name, description, inputSchema,
734
+ * and handler.
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
739
+ *
740
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
741
+ *
742
+ * registerToolFromSchema(server, {
743
+ * name: 'get-project',
744
+ * description: 'Get a project by ID',
745
+ * inputSchema: {
746
+ * type: 'object',
747
+ * properties: { id: { type: 'string', description: 'Project public ID' } },
748
+ * required: ['id'],
749
+ * },
750
+ * handler: async ({ id }) => ({
751
+ * content: [{ type: 'text', text: `Project: ${id}` }],
752
+ * }),
753
+ * });
754
+ * ```
755
+ */
756
+ declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
757
+ //#endregion
680
758
  //#region src/createGatedToolRegistrar.d.ts
681
759
  /** Resolved identity for a gated tool call. */
682
760
  type ToolIdentity = {
@@ -817,6 +895,22 @@ interface McpAuthOptions {
817
895
  resourceServerUrl?: string;
818
896
  /** URL of the OAuth Authorization Server that issues tokens for this resource. */
819
897
  authorizationServerUrl?: string;
898
+ /**
899
+ * Expected audience — the resource indicator (RFC 8707) this MCP server
900
+ * identifies as. When set, the verified token's `aud` claim must include at
901
+ * least one of these values, or the request is rejected with `401`. Without
902
+ * this check, a token minted for a *different* resource but signed by the
903
+ * same authorization server would still be accepted here — the classic
904
+ * confused-deputy risk RFC 8707 exists to close.
905
+ *
906
+ * Applies uniformly regardless of whether verification is done via
907
+ * `cognitoUserPool`, a custom `verifyToken`, or `@ttoss/auth-core/oidc`'s
908
+ * `createOidcVerifier` (which intentionally leaves audience validation to
909
+ * the caller for this reason).
910
+ *
911
+ * @example 'https://mcp.example.com'
912
+ */
913
+ resourceIndicator?: string | string[];
820
914
  }
821
915
  /**
822
916
  * Options for a single `apiCall` request.
@@ -1040,81 +1134,5 @@ interface McpRouterOptions {
1040
1134
  * ```
1041
1135
  */
1042
1136
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => Router<Application.DefaultState, Application.DefaultContext>;
1043
- /**
1044
- * A plain JSON Schema object (draft-07 compatible) describing the shape of a
1045
- * tool's input. Used with {@link registerToolFromSchema} as an alternative to
1046
- * providing a Zod shape, enabling lossless round-trips for schemas that contain
1047
- * features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
1048
- */
1049
- interface JsonObjectSchema {
1050
- type: 'object';
1051
- properties?: Record<string, unknown>;
1052
- required?: string[];
1053
- [key: string]: unknown;
1054
- }
1055
- /**
1056
- * Parameters accepted by {@link registerToolFromSchema}.
1057
- */
1058
- interface RegisterToolFromSchemaParams {
1059
- /** Unique tool name. */
1060
- name: string;
1061
- /** Human-readable description shown to the AI client. */
1062
- description?: string;
1063
- /**
1064
- * Plain JSON Schema that describes the tool's input object.
1065
- * This schema is forwarded verbatim over the MCP wire protocol, so any
1066
- * JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
1067
- * loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
1068
- */
1069
- inputSchema?: JsonObjectSchema;
1070
- /**
1071
- * Tool handler invoked when the AI client calls the tool.
1072
- * Receives the raw request arguments as a plain object (no Zod validation is
1073
- * applied, so the shape matches whatever the client sends).
1074
- */
1075
- handler: (args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>;
1076
- }
1077
- /**
1078
- * Registers a tool on an MCP server using a **plain JSON Schema** object for
1079
- * `inputSchema` instead of a Zod shape.
1080
- *
1081
- * This is useful when tool definitions are shared between the MCP server and an
1082
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
1083
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
1084
- * JSON-Schema→Zod conversion that would otherwise be required.
1085
- *
1086
- * Internally the helper:
1087
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
1088
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
1089
- * `tools/call` handler correctly routes requests and delivers raw args to
1090
- * your handler.
1091
- * 2. Stores the original JSON Schema and patches the `tools/list` response
1092
- * handler so clients receive the verbatim JSON Schema over the wire.
1093
- *
1094
- * @param server - The `McpServer` instance to register the tool on.
1095
- * @param params - Tool configuration including name, description, inputSchema,
1096
- * and handler.
1097
- *
1098
- * @example
1099
- * ```typescript
1100
- * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
1101
- *
1102
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
1103
- *
1104
- * registerToolFromSchema(server, {
1105
- * name: 'get-project',
1106
- * description: 'Get a project by ID',
1107
- * inputSchema: {
1108
- * type: 'object',
1109
- * properties: { id: { type: 'string', description: 'Project public ID' } },
1110
- * required: ['id'],
1111
- * },
1112
- * handler: async ({ id }) => ({
1113
- * content: [{ type: 'text', text: `Project: ${id}` }],
1114
- * }),
1115
- * });
1116
- * ```
1117
- */
1118
- declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1119
1137
  //#endregion
1120
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
1138
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/dist/index.mjs CHANGED
@@ -3,8 +3,8 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
3
3
  import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
4
4
  import { Router } from "@ttoss/http-server";
5
5
  import { authMiddleware } from "@ttoss/http-server-auth";
6
- import { z, z as z$1 } from "zod";
7
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { z, z as z$1 } from "zod";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
 
10
10
  //#region src/context.ts
@@ -21,6 +21,93 @@ var getIdentity = () => {
21
21
  return requestContextStore.getStore()?.identity;
22
22
  };
23
23
 
24
+ //#endregion
25
+ //#region src/registerToolFromSchema.ts
26
+ var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
27
+ var patchedServerSet = /* @__PURE__ */new WeakSet();
28
+ /**
29
+ * Registers a tool on an MCP server using a **plain JSON Schema** object for
30
+ * `inputSchema` instead of a Zod shape.
31
+ *
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
35
+ * JSON-Schema→Zod conversion that would otherwise be required.
36
+ *
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.
44
+ *
45
+ * @param server - The `McpServer` instance to register the tool on.
46
+ * @param params - Tool configuration including name, description, inputSchema,
47
+ * and handler.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
52
+ *
53
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
54
+ *
55
+ * registerToolFromSchema(server, {
56
+ * name: 'get-project',
57
+ * description: 'Get a project by ID',
58
+ * inputSchema: {
59
+ * type: 'object',
60
+ * properties: { id: { type: 'string', description: 'Project public ID' } },
61
+ * required: ['id'],
62
+ * },
63
+ * handler: async ({ id }) => ({
64
+ * content: [{ type: 'text', text: `Project: ${id}` }],
65
+ * }),
66
+ * });
67
+ * ```
68
+ */
69
+ var registerToolFromSchema = (server, params) => {
70
+ const {
71
+ name,
72
+ description,
73
+ inputSchema = {
74
+ type: "object",
75
+ properties: {}
76
+ },
77
+ handler
78
+ } = params;
79
+ if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
80
+ rawSchemaRegistryMap.get(server).set(name, inputSchema);
81
+ server.registerTool(name, {
82
+ description,
83
+ inputSchema: z$1.record(z$1.string(), z$1.unknown())
84
+ }, async args => {
85
+ return handler(args);
86
+ });
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
+ };
110
+
24
111
  //#endregion
25
112
  //#region src/createGatedToolRegistrar.ts
26
113
  var toolError = message => {
@@ -142,19 +229,39 @@ var createGatedToolRegistrar = ({
142
229
  //#region src/index.ts
143
230
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
144
231
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
232
+ /**
233
+ * Asserts that a verified token's `aud` claim includes at least one of the
234
+ * expected resource indicator values. `aud` may be a single string or an
235
+ * array of strings per the JWT spec (RFC 7519); both forms are checked.
236
+ */
237
+ var assertResourceIndicator = (payload, resourceIndicator) => {
238
+ const expected = Array.isArray(resourceIndicator) ? resourceIndicator : [resourceIndicator];
239
+ const aud = payload?.aud;
240
+ if (!(typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : []).some(audience => {
241
+ return typeof audience === "string" && expected.includes(audience);
242
+ })) throw new Error(`Token audience does not include the expected resource indicator (expected one of: ${expected.join(", ")}).`);
243
+ };
145
244
  /** Builds the token verifier from the MCP auth options. */
146
245
  var buildVerifyToken = auth => {
147
- if (auth.cognitoUserPool) {
148
- const verifier = CognitoJwtVerifier.create({
149
- tokenUse: "access",
150
- ...auth.cognitoUserPool
151
- });
152
- return token => {
153
- return verifier.verify(token);
154
- };
155
- }
156
- if (auth.verifyToken) return auth.verifyToken;
157
- throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
246
+ const verify = (() => {
247
+ if (auth.cognitoUserPool) {
248
+ const verifier = CognitoJwtVerifier.create({
249
+ tokenUse: "access",
250
+ ...auth.cognitoUserPool
251
+ });
252
+ return token => {
253
+ return verifier.verify(token);
254
+ };
255
+ }
256
+ if (auth.verifyToken) return auth.verifyToken;
257
+ throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
258
+ })();
259
+ if (!auth.resourceIndicator) return verify;
260
+ return async token => {
261
+ const payload = await verify(token);
262
+ assertResourceIndicator(payload, auth.resourceIndicator);
263
+ return payload;
264
+ };
158
265
  };
159
266
  /**
160
267
  * Generic HTTP helper for use inside MCP tool handlers.
@@ -437,90 +544,6 @@ var createMcpRouter = (server, options = {}) => {
437
544
  }
438
545
  return router;
439
546
  };
440
- var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
441
- var patchedServerSet = /* @__PURE__ */new WeakSet();
442
- /**
443
- * Registers a tool on an MCP server using a **plain JSON Schema** object for
444
- * `inputSchema` instead of a Zod shape.
445
- *
446
- * This is useful when tool definitions are shared between the MCP server and an
447
- * AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume a
448
- * plain JSON Schema at runtime. Using this helper eliminates the lossy
449
- * JSON-Schema→Zod conversion that would otherwise be required.
450
- *
451
- * Internally the helper:
452
- * 1. Registers the tool via the standard `McpServer.registerTool` API using a
453
- * Zod `z.record(z.unknown())` pass-through schema so that the existing MCP
454
- * `tools/call` handler correctly routes requests and delivers raw args to
455
- * your handler.
456
- * 2. Stores the original JSON Schema and patches the `tools/list` response
457
- * handler so clients receive the verbatim JSON Schema over the wire.
458
- *
459
- * @param server - The `McpServer` instance to register the tool on.
460
- * @param params - Tool configuration including name, description, inputSchema,
461
- * and handler.
462
- *
463
- * @example
464
- * ```typescript
465
- * import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
466
- *
467
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
468
- *
469
- * registerToolFromSchema(server, {
470
- * name: 'get-project',
471
- * description: 'Get a project by ID',
472
- * inputSchema: {
473
- * type: 'object',
474
- * properties: { id: { type: 'string', description: 'Project public ID' } },
475
- * required: ['id'],
476
- * },
477
- * handler: async ({ id }) => ({
478
- * content: [{ type: 'text', text: `Project: ${id}` }],
479
- * }),
480
- * });
481
- * ```
482
- */
483
- var registerToolFromSchema = (server, params) => {
484
- const {
485
- name,
486
- description,
487
- inputSchema = {
488
- type: "object",
489
- properties: {}
490
- },
491
- handler
492
- } = params;
493
- if (!rawSchemaRegistryMap.has(server)) rawSchemaRegistryMap.set(server, /* @__PURE__ */new Map());
494
- rawSchemaRegistryMap.get(server).set(name, inputSchema);
495
- server.registerTool(name, {
496
- description,
497
- inputSchema: z$1.record(z$1.string(), z$1.unknown())
498
- }, async args => {
499
- return handler(args);
500
- });
501
- if (!patchedServerSet.has(server)) {
502
- patchedServerSet.add(server);
503
- const rawServer = server.server;
504
- const origHandler = rawServer?._requestHandlers?.get("tools/list");
505
- 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.");
506
- if (origHandler) rawServer._requestHandlers.set("tools/list", async (rawRequest, extra) => {
507
- const result = await origHandler(rawRequest, extra);
508
- const schemas = rawSchemaRegistryMap.get(server);
509
- if (!schemas) return result;
510
- return {
511
- ...result,
512
- tools: result.tools.map(tool => {
513
- const raw = schemas.get(tool.name);
514
- if (raw !== void 0) return {
515
- ...tool,
516
- inputSchema: raw
517
- };
518
- return tool;
519
- })
520
- };
521
- });
522
- }
523
- };
524
547
 
525
548
  //#endregion
526
549
  export { McpServer, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -35,9 +35,9 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3",
38
- "@ttoss/auth-core": "^0.10.0",
39
- "@ttoss/http-server-auth": "^0.3.7",
40
- "@ttoss/http-server": "^0.7.1"
38
+ "@ttoss/auth-core": "^0.12.0",
39
+ "@ttoss/http-server": "^0.7.1",
40
+ "@ttoss/http-server-auth": "^0.5.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/koa": "^3.0.3",