@ttoss/http-server-mcp 0.19.0 → 0.20.0

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
@@ -366,6 +366,56 @@ createMcpRouter(mcpServer, {
366
366
 
367
367
  The discovery endpoint (`/.well-known/oauth-protected-resource`) remains publicly accessible even when `aliases` includes `'/'`.
368
368
 
369
+ ## Gated Tool Registrar
370
+
371
+ `createGatedToolRegistrar` wraps `server.registerTool` with a consistent authentication and authorization pipeline so you don't repeat the same boilerplate in every handler.
372
+
373
+ Every registered tool automatically:
374
+
375
+ 1. Resolves the caller's identity (defaults to `getIdentity()` from the request context).
376
+ 2. Checks that the caller holds a required OAuth scope — returns an `isError` result (not a throw) when the scope is absent.
377
+ 3. Runs any extra `gates` in order; a throwing gate rejects the call.
378
+ 4. Merges `buildContext` output into the handler args.
379
+ 5. Wraps `null`/`undefined` results in a "Not found" error result.
380
+ 6. Calls `onError` on throw before rethrowing, for telemetry.
381
+
382
+ ```typescript
383
+ import {
384
+ createGatedToolRegistrar,
385
+ getIdentity,
386
+ McpServer,
387
+ } from '@ttoss/http-server-mcp';
388
+ import { z } from 'zod';
389
+
390
+ const { register } = createGatedToolRegistrar({
391
+ server,
392
+ resolveIdentity: () => {
393
+ const jwt = getIdentity<{ sub: string; scope: string }>();
394
+ const scopes = jwt?.scope?.split(' ') ?? [];
395
+ return { userId: jwt!.sub, scopes };
396
+ },
397
+ buildContext: ({ userId }) => ({ tenantId: lookupTenant(userId) }),
398
+ onError: (err, { handler, identity }) => {
399
+ logger.error({ err, handler, userId: identity.userId });
400
+ },
401
+ });
402
+
403
+ register({
404
+ name: 'list-campaigns',
405
+ description: 'List all ad campaigns for the caller.',
406
+ requiredScope: 'campaigns:read',
407
+ inputSchema: { limit: z.number().optional() },
408
+ method: async ({ userId, tenantId, limit }) =>
409
+ fetchCampaigns(tenantId, limit),
410
+ });
411
+ ```
412
+
413
+ **`resolveIdentity`** is called once per invocation. Omit it to use the default `getIdentity()` from the MCP request context.
414
+
415
+ **`gates`** are async functions that receive the resolved identity and throw to reject. Use them for additional checks that span multiple tools (rate limiting, IP allowlists, etc.).
416
+
417
+ **`buildContext`** injects request-scoped values (DB clients, tenant IDs) into every handler call without threading them through each individual tool.
418
+
369
419
  ## Issuing tokens for MCP clients
370
420
 
371
421
  The `auth` option above covers the **resource-server** half of MCP authorization — it verifies tokens issued by an external authorization server (Cognito, Auth0, …). To make your own first-party server _issue_ the tokens an MCP client runs the full OAuth flow against, add the [`@ttoss/http-server-auth`](https://ttoss.dev/docs/modules/packages/http-server-auth) plugin's `oauthServer()` and pair it with `createMcpRouter({ auth: { verifyToken } })` so one deployment both issues and verifies tokens. See the [OAuth Authorization Server](https://ttoss.dev/docs/engineering/guidelines/oauth-authorization-server) guideline.
@@ -446,6 +496,28 @@ Returns the `WWW-Authenticate` header value for a 401 response, formatted per th
446
496
 
447
497
  **Returns:** `string` — The full `WWW-Authenticate` header value
448
498
 
499
+ ### `createGatedToolRegistrar(options)`
500
+
501
+ Factory that returns a `register` helper for tools that require authentication and a specific OAuth scope.
502
+
503
+ **Parameters (`options`):**
504
+
505
+ - `server` (`McpServer`) — The MCP server to register tools on.
506
+ - `resolveIdentity` (`() => ToolIdentity`, optional) — Called once per invocation to resolve `{ userId, scopes? }`. Defaults to `getIdentity()` from the request context.
507
+ - `gates` (`Array<(identity: ToolIdentity) => void | Promise<void>>`, optional) — Additional guards run after the scope check, in order. Throw to reject the call.
508
+ - `buildContext` (`(identity: ToolIdentity) => Record<string, unknown>`, optional) — Produces extra key-value pairs merged into every handler's args.
509
+ - `onError` (`(error, ctx) => void | Promise<void>`, optional) — Called when a handler throws, before the error is rethrown. Use for logging/telemetry.
510
+
511
+ **Returns:** `{ register: (def: GatedToolDef) => void }`
512
+
513
+ The `GatedToolDef` passed to `register` has:
514
+
515
+ - `name` — Tool name.
516
+ - `description` — Human-readable description.
517
+ - `requiredScope` — Single scope that must appear in `identity.scopes`.
518
+ - `inputSchema` — Zod field map or `ZodObject`, forwarded to `server.registerTool`.
519
+ - `method` — Async handler. Receives merged call args + `buildContext` output.
520
+
449
521
  ### `registerToolFromSchema(server, params)`
450
522
 
451
523
  Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
package/dist/index.cjs CHANGED
@@ -2,14 +2,122 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
- let node_async_hooks = require("node:async_hooks");
6
5
  let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
7
6
  let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
8
7
  let _ttoss_http_server = require("@ttoss/http-server");
9
8
  let _ttoss_http_server_auth = require("@ttoss/http-server-auth");
10
9
  let zod = require("zod");
10
+ let node_async_hooks = require("node:async_hooks");
11
11
  let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
12
12
 
13
+ //#region src/context.ts
14
+ var requestContextStore = new node_async_hooks.AsyncLocalStorage();
15
+ /**
16
+ * Returns the verified JWT payload for the current MCP request.
17
+ * Only available inside a tool handler when `auth` is configured on the router.
18
+ *
19
+ * Accepts an optional type parameter to avoid casting at call sites:
20
+ * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
21
+ * Omitting the type parameter keeps the return type as `unknown | undefined`.
22
+ */
23
+ var getIdentity = () => {
24
+ return requestContextStore.getStore()?.identity;
25
+ };
26
+
27
+ //#endregion
28
+ //#region src/createGatedToolRegistrar.ts
29
+ var toolError = message => {
30
+ return {
31
+ content: [{
32
+ type: "text",
33
+ text: JSON.stringify({
34
+ error: message
35
+ })
36
+ }],
37
+ isError: true
38
+ };
39
+ };
40
+ /**
41
+ * Factory that returns a `register` helper for MCP tools that require
42
+ * authentication and a specific OAuth scope.
43
+ *
44
+ * Every registered tool automatically:
45
+ * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
46
+ * `isError` result when the identity is absent (unauthenticated request).
47
+ * 2. Checks `requiredScope` — returns an `isError` result (not a throw) when
48
+ * the scope is absent, consistent with how MCP surfaces authorization errors.
49
+ * Scope semantics are intentionally simple (`Array.includes`). The package's
50
+ * `checkScopes` helper is not reused here because it throws rather than
51
+ * returning an `isError` result, and it reads identity from context itself.
52
+ * 3. Runs any extra `gates` in order; a throwing gate rejects the call.
53
+ * Gates own their own error handling; `onError` covers the tool handler only.
54
+ * 4. Merges `buildContext` output into the handler args.
55
+ * 5. Wraps `null`/`undefined` results in a "Not found" error result.
56
+ * 6. Calls `onError` on handler throw before rethrowing.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const { register } = createGatedToolRegistrar({
61
+ * server,
62
+ * resolveIdentity: () => {
63
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
64
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
65
+ * },
66
+ * });
67
+ *
68
+ * register({
69
+ * name: 'list-campaigns',
70
+ * description: 'List all ad campaigns for the caller.',
71
+ * requiredScope: 'campaigns:read',
72
+ * inputSchema: { limit: z.number().optional() },
73
+ * method: async ({ userId, limit }) => fetchCampaigns(userId, limit),
74
+ * });
75
+ * ```
76
+ */
77
+ var createGatedToolRegistrar = ({
78
+ server,
79
+ resolveIdentity = getIdentity,
80
+ gates = [],
81
+ onError,
82
+ buildContext
83
+ }) => {
84
+ const register = def => {
85
+ server.registerTool(def.name, {
86
+ description: def.description,
87
+ inputSchema: def.inputSchema
88
+ }, async args => {
89
+ const identity = resolveIdentity();
90
+ if (!identity) return toolError("Unauthorized");
91
+ if (!(Array.isArray(identity.scopes) ? identity.scopes : []).includes(def.requiredScope)) return toolError(`This tool requires the "${def.requiredScope}" scope.`);
92
+ for (const gate of gates) await gate(identity);
93
+ const extra = buildContext ? buildContext(identity) : {};
94
+ try {
95
+ const result = await def.method({
96
+ ...args,
97
+ ...extra
98
+ });
99
+ if (result == null) return toolError("Not found");
100
+ return {
101
+ content: [{
102
+ type: "text",
103
+ text: JSON.stringify(result)
104
+ }]
105
+ };
106
+ } catch (error) {
107
+ if (onError) await onError(error, {
108
+ handler: def.name,
109
+ identity
110
+ });
111
+ throw error;
112
+ }
113
+ });
114
+ };
115
+ return {
116
+ register
117
+ };
118
+ };
119
+
120
+ //#endregion
13
121
  //#region src/index.ts
14
122
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
15
123
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
@@ -27,7 +135,6 @@ var buildVerifyToken = auth => {
27
135
  if (auth.verifyToken) return auth.verifyToken;
28
136
  throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
29
137
  };
30
- var requestContextStore = new node_async_hooks.AsyncLocalStorage();
31
138
  /**
32
139
  * Generic HTTP helper for use inside MCP tool handlers.
33
140
  *
@@ -115,17 +222,6 @@ var apiCall = async (method, url, options) => {
115
222
  return response.text();
116
223
  };
117
224
  /**
118
- * Returns the verified JWT payload for the current MCP request.
119
- * Only available inside a tool handler when `auth` is configured on the router.
120
- *
121
- * Accepts an optional type parameter to avoid casting at call sites:
122
- * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
123
- * Omitting the type parameter keeps the return type as `unknown | undefined`.
124
- */
125
- var getIdentity = () => {
126
- return requestContextStore.getStore()?.identity;
127
- };
128
- /**
129
225
  * Asserts that the current request's token contains all required scopes.
130
226
  * Throws if any scope is missing — the MCP SDK catches this and returns a
131
227
  * tool error to the client. Use inside tool handlers for per-tool authorization.
@@ -414,6 +510,7 @@ Object.defineProperty(exports, 'McpServer', {
414
510
  });
415
511
  exports.apiCall = apiCall;
416
512
  exports.checkScopes = checkScopes;
513
+ exports.createGatedToolRegistrar = createGatedToolRegistrar;
417
514
  exports.createMcpRouter = createMcpRouter;
418
515
  exports.getIdentity = getIdentity;
419
516
  exports.registerToolFromSchema = registerToolFromSchema;
package/dist/index.d.cts CHANGED
@@ -665,6 +665,108 @@ declare namespace Application {
665
665
  const HttpError: typeof HttpErrors.HttpError;
666
666
  }
667
667
  //#endregion
668
+ //#region src/context.d.ts
669
+ /**
670
+ * Returns the verified JWT payload for the current MCP request.
671
+ * Only available inside a tool handler when `auth` is configured on the router.
672
+ *
673
+ * Accepts an optional type parameter to avoid casting at call sites:
674
+ * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
675
+ * Omitting the type parameter keeps the return type as `unknown | undefined`.
676
+ */
677
+ declare const getIdentity: <T = unknown>() => T | undefined;
678
+ //#endregion
679
+ //#region src/createGatedToolRegistrar.d.ts
680
+ /** Resolved identity for a gated tool call. */
681
+ type ToolIdentity = {
682
+ userId: string;
683
+ scopes?: string[];
684
+ };
685
+ /** Definition for a single gated tool. */
686
+ type GatedToolDef = {
687
+ /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
688
+ description: string; /** The single scope that must be present on the caller's token. */
689
+ requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
690
+ inputSchema: unknown; /** The tool handler. Receives merged call args + `buildContext` output. */
691
+ method: (args: Record<string, unknown>) => Promise<unknown>;
692
+ };
693
+ /** Options for {@link createGatedToolRegistrar}. */
694
+ type CreateGatedToolRegistrarOptions = {
695
+ /** The MCP server instance to register tools on. */server: McpServer$1;
696
+ /**
697
+ * Called once per tool invocation to resolve the caller's identity.
698
+ * Defaults to `getIdentity()` from the request context.
699
+ */
700
+ resolveIdentity?: () => ToolIdentity;
701
+ /**
702
+ * Additional authorization gates run after the scope check, in order.
703
+ * Throw to reject the call; return (or resolve) to continue.
704
+ * Gates own their own error handling — `onError` covers the tool handler only.
705
+ */
706
+ gates?: Array<(identity: ToolIdentity) => void | Promise<void>>;
707
+ /**
708
+ * Called when the tool **handler** throws. Use for error reporting/telemetry.
709
+ * The error is always rethrown after this hook completes.
710
+ * Note: scope-check failures and gate rejections do not trigger `onError`.
711
+ */
712
+ onError?: (error: unknown, ctx: {
713
+ handler: string;
714
+ identity: ToolIdentity;
715
+ }) => void | Promise<void>;
716
+ /**
717
+ * Called once per invocation to produce extra key-value pairs that are
718
+ * merged into the handler args. Use to inject request-scoped context
719
+ * (e.g. a database client keyed to the caller's tenant).
720
+ */
721
+ buildContext?: (identity: ToolIdentity) => Record<string, unknown>;
722
+ };
723
+ /**
724
+ * Factory that returns a `register` helper for MCP tools that require
725
+ * authentication and a specific OAuth scope.
726
+ *
727
+ * Every registered tool automatically:
728
+ * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
729
+ * `isError` result when the identity is absent (unauthenticated request).
730
+ * 2. Checks `requiredScope` — returns an `isError` result (not a throw) when
731
+ * the scope is absent, consistent with how MCP surfaces authorization errors.
732
+ * Scope semantics are intentionally simple (`Array.includes`). The package's
733
+ * `checkScopes` helper is not reused here because it throws rather than
734
+ * returning an `isError` result, and it reads identity from context itself.
735
+ * 3. Runs any extra `gates` in order; a throwing gate rejects the call.
736
+ * Gates own their own error handling; `onError` covers the tool handler only.
737
+ * 4. Merges `buildContext` output into the handler args.
738
+ * 5. Wraps `null`/`undefined` results in a "Not found" error result.
739
+ * 6. Calls `onError` on handler throw before rethrowing.
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * const { register } = createGatedToolRegistrar({
744
+ * server,
745
+ * resolveIdentity: () => {
746
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
747
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
748
+ * },
749
+ * });
750
+ *
751
+ * register({
752
+ * name: 'list-campaigns',
753
+ * description: 'List all ad campaigns for the caller.',
754
+ * requiredScope: 'campaigns:read',
755
+ * inputSchema: { limit: z.number().optional() },
756
+ * method: async ({ userId, limit }) => fetchCampaigns(userId, limit),
757
+ * });
758
+ * ```
759
+ */
760
+ declare const createGatedToolRegistrar: ({
761
+ server,
762
+ resolveIdentity,
763
+ gates,
764
+ onError,
765
+ buildContext
766
+ }: CreateGatedToolRegistrarOptions) => {
767
+ register: (def: GatedToolDef) => void;
768
+ };
769
+ //#endregion
668
770
  //#region src/index.d.ts
669
771
  type Context$1 = Application.Context;
670
772
  /** Amazon Cognito user pool configuration for JWT verification. */
@@ -787,15 +889,6 @@ interface ApiCallOptions {
787
889
  * ```
788
890
  */
789
891
  declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
790
- /**
791
- * Returns the verified JWT payload for the current MCP request.
792
- * Only available inside a tool handler when `auth` is configured on the router.
793
- *
794
- * Accepts an optional type parameter to avoid casting at call sites:
795
- * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
796
- * Omitting the type parameter keeps the return type as `unknown | undefined`.
797
- */
798
- declare const getIdentity: <T = unknown>() => T | undefined;
799
892
  /**
800
893
  * Asserts that the current request's token contains all required scopes.
801
894
  * Throws if any scope is missing — the MCP SDK catches this and returns a
@@ -1025,4 +1118,4 @@ interface RegisterToolFromSchemaParams {
1025
1118
  */
1026
1119
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1027
1120
  //#endregion
1028
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1121
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/dist/index.d.mts CHANGED
@@ -2,10 +2,11 @@
2
2
  /// <reference types="node" />
3
3
  import { Router } from "@ttoss/http-server";
4
4
  import { z } from "zod";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
7
8
  import accepts from "accepts";
8
- import { AsyncLocalStorage } from "async_hooks";
9
+ import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
9
10
  import Cookies from "cookies";
10
11
  import { EventEmitter } from "events";
11
12
  import { IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, Server, ServerResponse } from "http";
@@ -403,7 +404,7 @@ declare class Application<StateT = Application.DefaultState, ContextT = Applicat
403
404
  response: Application.BaseResponse;
404
405
  silent: boolean;
405
406
  keys: Keygrip | string[];
406
- ctxStorage: AsyncLocalStorage<ContextT> | undefined;
407
+ ctxStorage: AsyncLocalStorage$1<ContextT> | undefined;
407
408
  /**
408
409
  * @param {object} [options] Application options
409
410
  * @param {string} [options.env='development'] Environment
@@ -421,7 +422,7 @@ declare class Application<StateT = Application.DefaultState, ContextT = Applicat
421
422
  subdomainOffset?: number | undefined;
422
423
  proxyIpHeader?: string | undefined;
423
424
  maxIpsCount?: number | undefined;
424
- asyncLocalStorage?: boolean | AsyncLocalStorage<ContextT> | undefined;
425
+ asyncLocalStorage?: boolean | AsyncLocalStorage$1<ContextT> | undefined;
425
426
  });
426
427
  /**
427
428
  * Shorthand for:
@@ -665,6 +666,108 @@ declare namespace Application {
665
666
  const HttpError: typeof HttpErrors.HttpError;
666
667
  }
667
668
  //#endregion
669
+ //#region src/context.d.ts
670
+ /**
671
+ * Returns the verified JWT payload for the current MCP request.
672
+ * Only available inside a tool handler when `auth` is configured on the router.
673
+ *
674
+ * Accepts an optional type parameter to avoid casting at call sites:
675
+ * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
676
+ * Omitting the type parameter keeps the return type as `unknown | undefined`.
677
+ */
678
+ declare const getIdentity: <T = unknown>() => T | undefined;
679
+ //#endregion
680
+ //#region src/createGatedToolRegistrar.d.ts
681
+ /** Resolved identity for a gated tool call. */
682
+ type ToolIdentity = {
683
+ userId: string;
684
+ scopes?: string[];
685
+ };
686
+ /** Definition for a single gated tool. */
687
+ type GatedToolDef = {
688
+ /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
689
+ description: string; /** The single scope that must be present on the caller's token. */
690
+ requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
691
+ inputSchema: unknown; /** The tool handler. Receives merged call args + `buildContext` output. */
692
+ method: (args: Record<string, unknown>) => Promise<unknown>;
693
+ };
694
+ /** Options for {@link createGatedToolRegistrar}. */
695
+ type CreateGatedToolRegistrarOptions = {
696
+ /** The MCP server instance to register tools on. */server: McpServer$1;
697
+ /**
698
+ * Called once per tool invocation to resolve the caller's identity.
699
+ * Defaults to `getIdentity()` from the request context.
700
+ */
701
+ resolveIdentity?: () => ToolIdentity;
702
+ /**
703
+ * Additional authorization gates run after the scope check, in order.
704
+ * Throw to reject the call; return (or resolve) to continue.
705
+ * Gates own their own error handling — `onError` covers the tool handler only.
706
+ */
707
+ gates?: Array<(identity: ToolIdentity) => void | Promise<void>>;
708
+ /**
709
+ * Called when the tool **handler** throws. Use for error reporting/telemetry.
710
+ * The error is always rethrown after this hook completes.
711
+ * Note: scope-check failures and gate rejections do not trigger `onError`.
712
+ */
713
+ onError?: (error: unknown, ctx: {
714
+ handler: string;
715
+ identity: ToolIdentity;
716
+ }) => void | Promise<void>;
717
+ /**
718
+ * Called once per invocation to produce extra key-value pairs that are
719
+ * merged into the handler args. Use to inject request-scoped context
720
+ * (e.g. a database client keyed to the caller's tenant).
721
+ */
722
+ buildContext?: (identity: ToolIdentity) => Record<string, unknown>;
723
+ };
724
+ /**
725
+ * Factory that returns a `register` helper for MCP tools that require
726
+ * authentication and a specific OAuth scope.
727
+ *
728
+ * Every registered tool automatically:
729
+ * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
730
+ * `isError` result when the identity is absent (unauthenticated request).
731
+ * 2. Checks `requiredScope` — returns an `isError` result (not a throw) when
732
+ * the scope is absent, consistent with how MCP surfaces authorization errors.
733
+ * Scope semantics are intentionally simple (`Array.includes`). The package's
734
+ * `checkScopes` helper is not reused here because it throws rather than
735
+ * returning an `isError` result, and it reads identity from context itself.
736
+ * 3. Runs any extra `gates` in order; a throwing gate rejects the call.
737
+ * Gates own their own error handling; `onError` covers the tool handler only.
738
+ * 4. Merges `buildContext` output into the handler args.
739
+ * 5. Wraps `null`/`undefined` results in a "Not found" error result.
740
+ * 6. Calls `onError` on handler throw before rethrowing.
741
+ *
742
+ * @example
743
+ * ```typescript
744
+ * const { register } = createGatedToolRegistrar({
745
+ * server,
746
+ * resolveIdentity: () => {
747
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
748
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
749
+ * },
750
+ * });
751
+ *
752
+ * register({
753
+ * name: 'list-campaigns',
754
+ * description: 'List all ad campaigns for the caller.',
755
+ * requiredScope: 'campaigns:read',
756
+ * inputSchema: { limit: z.number().optional() },
757
+ * method: async ({ userId, limit }) => fetchCampaigns(userId, limit),
758
+ * });
759
+ * ```
760
+ */
761
+ declare const createGatedToolRegistrar: ({
762
+ server,
763
+ resolveIdentity,
764
+ gates,
765
+ onError,
766
+ buildContext
767
+ }: CreateGatedToolRegistrarOptions) => {
768
+ register: (def: GatedToolDef) => void;
769
+ };
770
+ //#endregion
668
771
  //#region src/index.d.ts
669
772
  type Context$1 = Application.Context;
670
773
  /** Amazon Cognito user pool configuration for JWT verification. */
@@ -787,15 +890,6 @@ interface ApiCallOptions {
787
890
  * ```
788
891
  */
789
892
  declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
790
- /**
791
- * Returns the verified JWT payload for the current MCP request.
792
- * Only available inside a tool handler when `auth` is configured on the router.
793
- *
794
- * Accepts an optional type parameter to avoid casting at call sites:
795
- * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
796
- * Omitting the type parameter keeps the return type as `unknown | undefined`.
797
- */
798
- declare const getIdentity: <T = unknown>() => T | undefined;
799
893
  /**
800
894
  * Asserts that the current request's token contains all required scopes.
801
895
  * Throws if any scope is missing — the MCP SDK catches this and returns a
@@ -1025,4 +1119,4 @@ interface RegisterToolFromSchemaParams {
1025
1119
  */
1026
1120
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1027
1121
  //#endregion
1028
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1122
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/dist/index.mjs CHANGED
@@ -1,12 +1,120 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
3
  import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
5
4
  import { Router } from "@ttoss/http-server";
6
5
  import { authMiddleware } from "@ttoss/http-server-auth";
7
6
  import { z, z as z$1 } from "zod";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
 
10
+ //#region src/context.ts
11
+ var requestContextStore = new AsyncLocalStorage();
12
+ /**
13
+ * Returns the verified JWT payload for the current MCP request.
14
+ * Only available inside a tool handler when `auth` is configured on the router.
15
+ *
16
+ * Accepts an optional type parameter to avoid casting at call sites:
17
+ * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
18
+ * Omitting the type parameter keeps the return type as `unknown | undefined`.
19
+ */
20
+ var getIdentity = () => {
21
+ return requestContextStore.getStore()?.identity;
22
+ };
23
+
24
+ //#endregion
25
+ //#region src/createGatedToolRegistrar.ts
26
+ var toolError = message => {
27
+ return {
28
+ content: [{
29
+ type: "text",
30
+ text: JSON.stringify({
31
+ error: message
32
+ })
33
+ }],
34
+ isError: true
35
+ };
36
+ };
37
+ /**
38
+ * Factory that returns a `register` helper for MCP tools that require
39
+ * authentication and a specific OAuth scope.
40
+ *
41
+ * Every registered tool automatically:
42
+ * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
43
+ * `isError` result when the identity is absent (unauthenticated request).
44
+ * 2. Checks `requiredScope` — returns an `isError` result (not a throw) when
45
+ * the scope is absent, consistent with how MCP surfaces authorization errors.
46
+ * Scope semantics are intentionally simple (`Array.includes`). The package's
47
+ * `checkScopes` helper is not reused here because it throws rather than
48
+ * returning an `isError` result, and it reads identity from context itself.
49
+ * 3. Runs any extra `gates` in order; a throwing gate rejects the call.
50
+ * Gates own their own error handling; `onError` covers the tool handler only.
51
+ * 4. Merges `buildContext` output into the handler args.
52
+ * 5. Wraps `null`/`undefined` results in a "Not found" error result.
53
+ * 6. Calls `onError` on handler throw before rethrowing.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const { register } = createGatedToolRegistrar({
58
+ * server,
59
+ * resolveIdentity: () => {
60
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
61
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
62
+ * },
63
+ * });
64
+ *
65
+ * register({
66
+ * name: 'list-campaigns',
67
+ * description: 'List all ad campaigns for the caller.',
68
+ * requiredScope: 'campaigns:read',
69
+ * inputSchema: { limit: z.number().optional() },
70
+ * method: async ({ userId, limit }) => fetchCampaigns(userId, limit),
71
+ * });
72
+ * ```
73
+ */
74
+ var createGatedToolRegistrar = ({
75
+ server,
76
+ resolveIdentity = getIdentity,
77
+ gates = [],
78
+ onError,
79
+ buildContext
80
+ }) => {
81
+ const register = def => {
82
+ server.registerTool(def.name, {
83
+ description: def.description,
84
+ inputSchema: def.inputSchema
85
+ }, async args => {
86
+ const identity = resolveIdentity();
87
+ if (!identity) return toolError("Unauthorized");
88
+ if (!(Array.isArray(identity.scopes) ? identity.scopes : []).includes(def.requiredScope)) return toolError(`This tool requires the "${def.requiredScope}" scope.`);
89
+ for (const gate of gates) await gate(identity);
90
+ const extra = buildContext ? buildContext(identity) : {};
91
+ try {
92
+ const result = await def.method({
93
+ ...args,
94
+ ...extra
95
+ });
96
+ if (result == null) return toolError("Not found");
97
+ return {
98
+ content: [{
99
+ type: "text",
100
+ text: JSON.stringify(result)
101
+ }]
102
+ };
103
+ } catch (error) {
104
+ if (onError) await onError(error, {
105
+ handler: def.name,
106
+ identity
107
+ });
108
+ throw error;
109
+ }
110
+ });
111
+ };
112
+ return {
113
+ register
114
+ };
115
+ };
116
+
117
+ //#endregion
10
118
  //#region src/index.ts
11
119
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
12
120
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
@@ -24,7 +132,6 @@ var buildVerifyToken = auth => {
24
132
  if (auth.verifyToken) return auth.verifyToken;
25
133
  throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
26
134
  };
27
- var requestContextStore = new AsyncLocalStorage();
28
135
  /**
29
136
  * Generic HTTP helper for use inside MCP tool handlers.
30
137
  *
@@ -112,17 +219,6 @@ var apiCall = async (method, url, options) => {
112
219
  return response.text();
113
220
  };
114
221
  /**
115
- * Returns the verified JWT payload for the current MCP request.
116
- * Only available inside a tool handler when `auth` is configured on the router.
117
- *
118
- * Accepts an optional type parameter to avoid casting at call sites:
119
- * `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
120
- * Omitting the type parameter keeps the return type as `unknown | undefined`.
121
- */
122
- var getIdentity = () => {
123
- return requestContextStore.getStore()?.identity;
124
- };
125
- /**
126
222
  * Asserts that the current request's token contains all required scopes.
127
223
  * Throws if any scope is missing — the MCP SDK catches this and returns a
128
224
  * tool error to the client. Use inside tool handlers for per-tool authorization.
@@ -403,4 +499,4 @@ var registerToolFromSchema = (server, params) => {
403
499
  };
404
500
 
405
501
  //#endregion
406
- export { McpServer, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
502
+ 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.19.0",
3
+ "version": "0.20.0",
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.8.0",
39
- "@ttoss/http-server-auth": "^0.3.5",
40
- "@ttoss/http-server": "^0.7.1"
38
+ "@ttoss/auth-core": "^0.10.0",
39
+ "@ttoss/http-server": "^0.7.1",
40
+ "@ttoss/http-server-auth": "^0.3.7"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/koa": "^3.0.3",