@ttoss/http-server-mcp 0.19.1 → 0.21.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,74 @@ 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 global `gates` then per-tool `def.gates` in order; a throwing gate rejects the call. Both receive `ToolCallContext` (identity **and** the validated call args).
378
+ 4. Merges `buildContext` output into the handler args.
379
+ 5. Wraps `null`/`undefined` results in a configurable "Not found" error result.
380
+ 6. Calls `onError` on handler throw before rethrowing, for telemetry. Gate/scope failures do **not** trigger `onError`.
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: ({ identity }) => ({ tenantId: lookupTenant(identity.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`** (global and per-tool) are async functions that receive a `ToolCallContext` — both the resolved identity and the validated call args — and throw to reject. The full context lets gates vary their predicate based on what the caller is asking for, not just who they are.
416
+
417
+ ```typescript
418
+ register({
419
+ name: 'activate-ad-account',
420
+ description: 'Activate or deactivate an ad account.',
421
+ requiredScope: 'accounts:write',
422
+ inputSchema: { accountId: z.number(), isActive: z.boolean() },
423
+ // arg-conditional gate: activating requires more checks than deactivating
424
+ gates: [
425
+ ({ args }) =>
426
+ args.isActive
427
+ ? checkSubscriptionGates(['mustNotExceedMaxActive', 'mustHaveBudget'])
428
+ : checkSubscriptionGates(['mustIncludeService']),
429
+ ],
430
+ method: async ({ userId, accountId, isActive }) =>
431
+ toggleAdAccount(userId, accountId, isActive),
432
+ });
433
+ ```
434
+
435
+ **`buildContext`** injects request-scoped values (DB clients, tenant IDs) into every handler call without threading them through each individual tool. It receives the full `ToolCallContext` so context can vary by identity or by call args.
436
+
369
437
  ## Issuing tokens for MCP clients
370
438
 
371
439
  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 +514,37 @@ Returns the `WWW-Authenticate` header value for a 401 response, formatted per th
446
514
 
447
515
  **Returns:** `string` — The full `WWW-Authenticate` header value
448
516
 
517
+ ### `createGatedToolRegistrar(options)`
518
+
519
+ Factory that returns a `register` helper for tools that require authentication and a specific OAuth scope.
520
+
521
+ **Parameters (`options`):**
522
+
523
+ - `server` (`McpServer`) — The MCP server to register tools on.
524
+ - `resolveIdentity` (`() => ToolIdentity`, optional) — Called once per invocation to resolve `{ userId, scopes? }`. Defaults to `getIdentity()` from the request context.
525
+ - `gates` (`Array<(ctx: ToolCallContext) => void | Promise<void>>`, optional) — Global guards run after the scope check, in order, before any per-tool gates. Each receives `{ identity, args, handler }`. Throw to reject the call.
526
+ - `enforceScope` (`boolean`, optional, default `true`) — When `true`, checks `def.requiredScope` against `identity.scopes` and returns an `isError` result on mismatch. Set to `false` when all authorization is handled by `gates`.
527
+ - `buildContext` (`(ctx: ToolCallContext) => Record<string, unknown>`, optional) — Produces extra key-value pairs merged into every handler's args. Receives the full `ToolCallContext` so context can vary by identity or by call args.
528
+ - `onError` (`(error, ctx: ToolCallContext) => void | Promise<void>`, optional) — Called when the handler throws, before the error is rethrown. Gate/scope failures do **not** trigger this hook.
529
+ - `notFoundMessage` (`string`, optional, default `"Not found"`) — `isError` message returned when the handler resolves to `null`/`undefined`.
530
+
531
+ **Returns:** `{ register: (def: GatedToolDef) => void }`
532
+
533
+ The `GatedToolDef` passed to `register` has:
534
+
535
+ - `name` — Tool name.
536
+ - `description` — Human-readable description.
537
+ - `requiredScope` — Single scope that must appear in `identity.scopes`.
538
+ - `inputSchema` — Zod field map or `ZodObject`, forwarded to `server.registerTool`.
539
+ - `gates` (`Array<(ctx: ToolCallContext) => void | Promise<void>>`, optional) — Per-tool guards appended after the global `gates`. Receive the full `ToolCallContext` enabling arg-conditional authorization.
540
+ - `method` — Async handler. Receives merged call args + `buildContext` output.
541
+
542
+ **`ToolCallContext`** is the object passed to gates, `buildContext`, and `onError`:
543
+
544
+ - `identity` (`ToolIdentity`) — The resolved caller identity (`{ userId, scopes? }`).
545
+ - `args` (`Record<string, unknown>`) — The validated tool input (post SDK parse).
546
+ - `handler` (`string`) — The tool name, for error attribution.
547
+
449
548
  ### `registerToolFromSchema(server, params)`
450
549
 
451
550
  Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
package/dist/index.cjs CHANGED
@@ -2,14 +2,145 @@
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. When `enforceScope` is `true` (default), checks `requiredScope` — returns
48
+ * an `isError` result when the scope is absent, consistent with how MCP
49
+ * surfaces authorization errors. The package's `checkScopes` helper is not
50
+ * reused here because it throws rather than returning an `isError` result,
51
+ * and it reads identity from context itself.
52
+ * 3. Runs global `gates` then per-`register` `def.gates` in order; a throwing
53
+ * gate rejects the call. Every gate receives the full {@link ToolCallContext}
54
+ * (identity **and** the validated call args), enabling arg-conditional gates.
55
+ * Gates own their own error handling; `onError` covers the tool handler only.
56
+ * 4. Merges `buildContext` output into the handler args.
57
+ * 5. Wraps `null`/`undefined` results in a configurable "Not found" error result.
58
+ * 6. Calls `onError` on handler throw before rethrowing.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const { register } = createGatedToolRegistrar({
63
+ * server,
64
+ * resolveIdentity: () => {
65
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
66
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
67
+ * },
68
+ * });
69
+ *
70
+ * register({
71
+ * name: 'activate-campaign',
72
+ * description: 'Activate or deactivate a campaign.',
73
+ * requiredScope: 'campaigns:write',
74
+ * inputSchema: { isActive: z.boolean() },
75
+ * // arg-conditional gate: active vs. inactive have different restrictions
76
+ * gates: [
77
+ * ({ args }) =>
78
+ * args.isActive
79
+ * ? subscriptionGate(['mustNotExceedMaxActive', 'mustHaveBudget'])
80
+ * : subscriptionGate(['mustIncludeService']),
81
+ * ],
82
+ * method: async ({ userId, isActive }) => toggleCampaign(userId, isActive),
83
+ * });
84
+ * ```
85
+ */
86
+ var checkScope = (identity, requiredScope) => {
87
+ if (!(Array.isArray(identity.scopes) ? identity.scopes : []).includes(requiredScope)) return toolError(`This tool requires the "${requiredScope}" scope.`);
88
+ return null;
89
+ };
90
+ var runGates = async (allGates, ctx) => {
91
+ for (const gate of allGates) await gate(ctx);
92
+ };
93
+ var createGatedToolRegistrar = ({
94
+ server,
95
+ resolveIdentity = getIdentity,
96
+ gates = [],
97
+ enforceScope = true,
98
+ onError,
99
+ buildContext,
100
+ notFoundMessage = "Not found"
101
+ }) => {
102
+ const register = def => {
103
+ server.registerTool(def.name, {
104
+ description: def.description,
105
+ inputSchema: def.inputSchema
106
+ }, async args => {
107
+ const identity = resolveIdentity();
108
+ if (!identity) return toolError("Unauthorized");
109
+ const ctx = {
110
+ identity,
111
+ args,
112
+ handler: def.name
113
+ };
114
+ if (enforceScope) {
115
+ const scopeError = checkScope(identity, def.requiredScope);
116
+ if (scopeError) return scopeError;
117
+ }
118
+ await runGates([...gates, ...(def.gates ?? [])], ctx);
119
+ const extra = buildContext ? buildContext(ctx) : {};
120
+ try {
121
+ const result = await def.method({
122
+ ...args,
123
+ ...extra
124
+ });
125
+ if (result == null) return toolError(notFoundMessage);
126
+ return {
127
+ content: [{
128
+ type: "text",
129
+ text: JSON.stringify(result)
130
+ }]
131
+ };
132
+ } catch (error) {
133
+ if (onError) await onError(error, ctx);
134
+ throw error;
135
+ }
136
+ });
137
+ };
138
+ return {
139
+ register
140
+ };
141
+ };
142
+
143
+ //#endregion
13
144
  //#region src/index.ts
14
145
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
15
146
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
@@ -27,7 +158,6 @@ var buildVerifyToken = auth => {
27
158
  if (auth.verifyToken) return auth.verifyToken;
28
159
  throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
29
160
  };
30
- var requestContextStore = new node_async_hooks.AsyncLocalStorage();
31
161
  /**
32
162
  * Generic HTTP helper for use inside MCP tool handlers.
33
163
  *
@@ -115,17 +245,6 @@ var apiCall = async (method, url, options) => {
115
245
  return response.text();
116
246
  };
117
247
  /**
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
248
  * Asserts that the current request's token contains all required scopes.
130
249
  * Throws if any scope is missing — the MCP SDK catches this and returns a
131
250
  * tool error to the client. Use inside tool handlers for per-tool authorization.
@@ -414,6 +533,7 @@ Object.defineProperty(exports, 'McpServer', {
414
533
  });
415
534
  exports.apiCall = apiCall;
416
535
  exports.checkScopes = checkScopes;
536
+ exports.createGatedToolRegistrar = createGatedToolRegistrar;
417
537
  exports.createMcpRouter = createMcpRouter;
418
538
  exports.getIdentity = getIdentity;
419
539
  exports.registerToolFromSchema = registerToolFromSchema;
package/dist/index.d.cts CHANGED
@@ -665,6 +665,106 @@ 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
+ /**
686
+ * Full context passed to gates, `buildContext`, and `onError` on every tool
687
+ * invocation. Having all three fields in one object means gate callbacks can
688
+ * be both identity-aware and args-aware without multiple parameters.
689
+ */
690
+ type ToolCallContext = {
691
+ /** The resolved caller identity. */identity: ToolIdentity;
692
+ /**
693
+ * The validated tool input (post SDK parse). Present for gates and
694
+ * `buildContext`. Arg-conditional gates read this to vary their predicate
695
+ * per call.
696
+ */
697
+ args: Record<string, unknown>; /** Tool name, for error attribution and gate labelling. */
698
+ handler: string;
699
+ };
700
+ /** Definition for a single gated tool. */
701
+ type GatedToolDef = {
702
+ /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
703
+ description: string; /** The single scope that must be present on the caller's token. */
704
+ requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
705
+ inputSchema: unknown;
706
+ /**
707
+ * Per-tool gates merged after the global `gates`. Useful for arg-conditional
708
+ * authorization (e.g. different subscription tiers based on call args).
709
+ * Each gate receives the full {@link ToolCallContext} (identity + args).
710
+ * Throw to reject the call; return (or resolve) to continue.
711
+ */
712
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>; /** The tool handler. Receives merged call args + `buildContext` output. */
713
+ method: (args: Record<string, unknown>) => Promise<unknown>;
714
+ };
715
+ /** Options for {@link createGatedToolRegistrar}. */
716
+ type CreateGatedToolRegistrarOptions = {
717
+ /** The MCP server instance to register tools on. */server: McpServer$1;
718
+ /**
719
+ * Called once per tool invocation to resolve the caller's identity.
720
+ * Defaults to `getIdentity()` from the request context.
721
+ */
722
+ resolveIdentity?: () => ToolIdentity;
723
+ /**
724
+ * Global authorization gates run after the scope check, in order, before
725
+ * per-tool gates. Each receives the full {@link ToolCallContext} — both
726
+ * identity and the validated call args — so gate predicates may be
727
+ * conditional on either.
728
+ * Throw to reject the call; return (or resolve) to continue.
729
+ * Gates own their own error handling — `onError` covers the tool handler only.
730
+ */
731
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>;
732
+ /**
733
+ * When `true` (default), checks `def.requiredScope` against `identity.scopes`
734
+ * and returns an `isError` result when the scope is absent. Set to `false`
735
+ * to skip the scope check (e.g. when all scopes are enforced by `gates`).
736
+ */
737
+ enforceScope?: boolean;
738
+ /**
739
+ * Called when the tool **handler** throws. Use for error reporting/telemetry.
740
+ * The error is always rethrown after this hook completes.
741
+ * Note: scope-check failures and gate rejections do not trigger `onError`.
742
+ */
743
+ onError?: (error: unknown, ctx: ToolCallContext) => void | Promise<void>;
744
+ /**
745
+ * Called once per invocation to produce extra key-value pairs that are
746
+ * merged into the handler args. Receives the full {@link ToolCallContext}
747
+ * so context can vary by identity or by call args.
748
+ */
749
+ buildContext?: (ctx: ToolCallContext) => Record<string, unknown>;
750
+ /**
751
+ * Message returned as an `isError` result when the handler resolves to
752
+ * `null` or `undefined`. Defaults to `"Not found"`.
753
+ */
754
+ notFoundMessage?: string;
755
+ };
756
+ declare const createGatedToolRegistrar: ({
757
+ server,
758
+ resolveIdentity,
759
+ gates,
760
+ enforceScope,
761
+ onError,
762
+ buildContext,
763
+ notFoundMessage
764
+ }: CreateGatedToolRegistrarOptions) => {
765
+ register: (def: GatedToolDef) => void;
766
+ };
767
+ //#endregion
668
768
  //#region src/index.d.ts
669
769
  type Context$1 = Application.Context;
670
770
  /** Amazon Cognito user pool configuration for JWT verification. */
@@ -787,15 +887,6 @@ interface ApiCallOptions {
787
887
  * ```
788
888
  */
789
889
  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
890
  /**
800
891
  * Asserts that the current request's token contains all required scopes.
801
892
  * Throws if any scope is missing — the MCP SDK catches this and returns a
@@ -1025,4 +1116,4 @@ interface RegisterToolFromSchemaParams {
1025
1116
  */
1026
1117
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1027
1118
  //#endregion
1028
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1119
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolCallContext, 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,106 @@ 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
+ /**
687
+ * Full context passed to gates, `buildContext`, and `onError` on every tool
688
+ * invocation. Having all three fields in one object means gate callbacks can
689
+ * be both identity-aware and args-aware without multiple parameters.
690
+ */
691
+ type ToolCallContext = {
692
+ /** The resolved caller identity. */identity: ToolIdentity;
693
+ /**
694
+ * The validated tool input (post SDK parse). Present for gates and
695
+ * `buildContext`. Arg-conditional gates read this to vary their predicate
696
+ * per call.
697
+ */
698
+ args: Record<string, unknown>; /** Tool name, for error attribution and gate labelling. */
699
+ handler: string;
700
+ };
701
+ /** Definition for a single gated tool. */
702
+ type GatedToolDef = {
703
+ /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
704
+ description: string; /** The single scope that must be present on the caller's token. */
705
+ requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
706
+ inputSchema: unknown;
707
+ /**
708
+ * Per-tool gates merged after the global `gates`. Useful for arg-conditional
709
+ * authorization (e.g. different subscription tiers based on call args).
710
+ * Each gate receives the full {@link ToolCallContext} (identity + args).
711
+ * Throw to reject the call; return (or resolve) to continue.
712
+ */
713
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>; /** The tool handler. Receives merged call args + `buildContext` output. */
714
+ method: (args: Record<string, unknown>) => Promise<unknown>;
715
+ };
716
+ /** Options for {@link createGatedToolRegistrar}. */
717
+ type CreateGatedToolRegistrarOptions = {
718
+ /** The MCP server instance to register tools on. */server: McpServer$1;
719
+ /**
720
+ * Called once per tool invocation to resolve the caller's identity.
721
+ * Defaults to `getIdentity()` from the request context.
722
+ */
723
+ resolveIdentity?: () => ToolIdentity;
724
+ /**
725
+ * Global authorization gates run after the scope check, in order, before
726
+ * per-tool gates. Each receives the full {@link ToolCallContext} — both
727
+ * identity and the validated call args — so gate predicates may be
728
+ * conditional on either.
729
+ * Throw to reject the call; return (or resolve) to continue.
730
+ * Gates own their own error handling — `onError` covers the tool handler only.
731
+ */
732
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>;
733
+ /**
734
+ * When `true` (default), checks `def.requiredScope` against `identity.scopes`
735
+ * and returns an `isError` result when the scope is absent. Set to `false`
736
+ * to skip the scope check (e.g. when all scopes are enforced by `gates`).
737
+ */
738
+ enforceScope?: boolean;
739
+ /**
740
+ * Called when the tool **handler** throws. Use for error reporting/telemetry.
741
+ * The error is always rethrown after this hook completes.
742
+ * Note: scope-check failures and gate rejections do not trigger `onError`.
743
+ */
744
+ onError?: (error: unknown, ctx: ToolCallContext) => void | Promise<void>;
745
+ /**
746
+ * Called once per invocation to produce extra key-value pairs that are
747
+ * merged into the handler args. Receives the full {@link ToolCallContext}
748
+ * so context can vary by identity or by call args.
749
+ */
750
+ buildContext?: (ctx: ToolCallContext) => Record<string, unknown>;
751
+ /**
752
+ * Message returned as an `isError` result when the handler resolves to
753
+ * `null` or `undefined`. Defaults to `"Not found"`.
754
+ */
755
+ notFoundMessage?: string;
756
+ };
757
+ declare const createGatedToolRegistrar: ({
758
+ server,
759
+ resolveIdentity,
760
+ gates,
761
+ enforceScope,
762
+ onError,
763
+ buildContext,
764
+ notFoundMessage
765
+ }: CreateGatedToolRegistrarOptions) => {
766
+ register: (def: GatedToolDef) => void;
767
+ };
768
+ //#endregion
668
769
  //#region src/index.d.ts
669
770
  type Context$1 = Application.Context;
670
771
  /** Amazon Cognito user pool configuration for JWT verification. */
@@ -787,15 +888,6 @@ interface ApiCallOptions {
787
888
  * ```
788
889
  */
789
890
  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
891
  /**
800
892
  * Asserts that the current request's token contains all required scopes.
801
893
  * Throws if any scope is missing — the MCP SDK catches this and returns a
@@ -1025,4 +1117,4 @@ interface RegisterToolFromSchemaParams {
1025
1117
  */
1026
1118
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1027
1119
  //#endregion
1028
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1120
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
package/dist/index.mjs CHANGED
@@ -1,12 +1,143 @@
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. When `enforceScope` is `true` (default), checks `requiredScope` — returns
45
+ * an `isError` result when the scope is absent, consistent with how MCP
46
+ * surfaces authorization errors. The package's `checkScopes` helper is not
47
+ * reused here because it throws rather than returning an `isError` result,
48
+ * and it reads identity from context itself.
49
+ * 3. Runs global `gates` then per-`register` `def.gates` in order; a throwing
50
+ * gate rejects the call. Every gate receives the full {@link ToolCallContext}
51
+ * (identity **and** the validated call args), enabling arg-conditional gates.
52
+ * Gates own their own error handling; `onError` covers the tool handler only.
53
+ * 4. Merges `buildContext` output into the handler args.
54
+ * 5. Wraps `null`/`undefined` results in a configurable "Not found" error result.
55
+ * 6. Calls `onError` on handler throw before rethrowing.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const { register } = createGatedToolRegistrar({
60
+ * server,
61
+ * resolveIdentity: () => {
62
+ * const jwt = getIdentity<{ sub: string; scopes: string[] }>();
63
+ * return { userId: jwt!.sub, scopes: jwt!.scopes };
64
+ * },
65
+ * });
66
+ *
67
+ * register({
68
+ * name: 'activate-campaign',
69
+ * description: 'Activate or deactivate a campaign.',
70
+ * requiredScope: 'campaigns:write',
71
+ * inputSchema: { isActive: z.boolean() },
72
+ * // arg-conditional gate: active vs. inactive have different restrictions
73
+ * gates: [
74
+ * ({ args }) =>
75
+ * args.isActive
76
+ * ? subscriptionGate(['mustNotExceedMaxActive', 'mustHaveBudget'])
77
+ * : subscriptionGate(['mustIncludeService']),
78
+ * ],
79
+ * method: async ({ userId, isActive }) => toggleCampaign(userId, isActive),
80
+ * });
81
+ * ```
82
+ */
83
+ var checkScope = (identity, requiredScope) => {
84
+ if (!(Array.isArray(identity.scopes) ? identity.scopes : []).includes(requiredScope)) return toolError(`This tool requires the "${requiredScope}" scope.`);
85
+ return null;
86
+ };
87
+ var runGates = async (allGates, ctx) => {
88
+ for (const gate of allGates) await gate(ctx);
89
+ };
90
+ var createGatedToolRegistrar = ({
91
+ server,
92
+ resolveIdentity = getIdentity,
93
+ gates = [],
94
+ enforceScope = true,
95
+ onError,
96
+ buildContext,
97
+ notFoundMessage = "Not found"
98
+ }) => {
99
+ const register = def => {
100
+ server.registerTool(def.name, {
101
+ description: def.description,
102
+ inputSchema: def.inputSchema
103
+ }, async args => {
104
+ const identity = resolveIdentity();
105
+ if (!identity) return toolError("Unauthorized");
106
+ const ctx = {
107
+ identity,
108
+ args,
109
+ handler: def.name
110
+ };
111
+ if (enforceScope) {
112
+ const scopeError = checkScope(identity, def.requiredScope);
113
+ if (scopeError) return scopeError;
114
+ }
115
+ await runGates([...gates, ...(def.gates ?? [])], ctx);
116
+ const extra = buildContext ? buildContext(ctx) : {};
117
+ try {
118
+ const result = await def.method({
119
+ ...args,
120
+ ...extra
121
+ });
122
+ if (result == null) return toolError(notFoundMessage);
123
+ return {
124
+ content: [{
125
+ type: "text",
126
+ text: JSON.stringify(result)
127
+ }]
128
+ };
129
+ } catch (error) {
130
+ if (onError) await onError(error, ctx);
131
+ throw error;
132
+ }
133
+ });
134
+ };
135
+ return {
136
+ register
137
+ };
138
+ };
139
+
140
+ //#endregion
10
141
  //#region src/index.ts
11
142
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
12
143
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
@@ -24,7 +155,6 @@ var buildVerifyToken = auth => {
24
155
  if (auth.verifyToken) return auth.verifyToken;
25
156
  throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
26
157
  };
27
- var requestContextStore = new AsyncLocalStorage();
28
158
  /**
29
159
  * Generic HTTP helper for use inside MCP tool handlers.
30
160
  *
@@ -112,17 +242,6 @@ var apiCall = async (method, url, options) => {
112
242
  return response.text();
113
243
  };
114
244
  /**
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
245
  * Asserts that the current request's token contains all required scopes.
127
246
  * Throws if any scope is missing — the MCP SDK catches this and returns a
128
247
  * tool error to the client. Use inside tool handlers for per-tool authorization.
@@ -403,4 +522,4 @@ var registerToolFromSchema = (server, params) => {
403
522
  };
404
523
 
405
524
  //#endregion
406
- export { McpServer, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
525
+ 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.1",
3
+ "version": "0.21.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -36,8 +36,8 @@
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3",
38
38
  "@ttoss/http-server": "^0.7.1",
39
- "@ttoss/http-server-auth": "^0.3.6",
40
- "@ttoss/auth-core": "^0.9.0"
39
+ "@ttoss/auth-core": "^0.10.0",
40
+ "@ttoss/http-server-auth": "^0.3.7"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/koa": "^3.0.3",