@ttoss/http-server-mcp 0.20.0 → 0.21.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
@@ -374,10 +374,10 @@ Every registered tool automatically:
374
374
 
375
375
  1. Resolves the caller's identity (defaults to `getIdentity()` from the request context).
376
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.
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
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.
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
381
 
382
382
  ```typescript
383
383
  import {
@@ -394,7 +394,7 @@ const { register } = createGatedToolRegistrar({
394
394
  const scopes = jwt?.scope?.split(' ') ?? [];
395
395
  return { userId: jwt!.sub, scopes };
396
396
  },
397
- buildContext: ({ userId }) => ({ tenantId: lookupTenant(userId) }),
397
+ buildContext: ({ identity }) => ({ tenantId: lookupTenant(identity.userId) }),
398
398
  onError: (err, { handler, identity }) => {
399
399
  logger.error({ err, handler, userId: identity.userId });
400
400
  },
@@ -412,9 +412,27 @@ register({
412
412
 
413
413
  **`resolveIdentity`** is called once per invocation. Omit it to use the default `getIdentity()` from the MCP request context.
414
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.).
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
416
 
417
- **`buildContext`** injects request-scoped values (DB clients, tenant IDs) into every handler call without threading them through each individual tool.
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.
418
436
 
419
437
  ## Issuing tokens for MCP clients
420
438
 
@@ -504,9 +522,11 @@ Factory that returns a `register` helper for tools that require authentication a
504
522
 
505
523
  - `server` (`McpServer`) — The MCP server to register tools on.
506
524
  - `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.
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`.
510
530
 
511
531
  **Returns:** `{ register: (def: GatedToolDef) => void }`
512
532
 
@@ -516,8 +536,15 @@ The `GatedToolDef` passed to `register` has:
516
536
  - `description` — Human-readable description.
517
537
  - `requiredScope` — Single scope that must appear in `identity.scopes`.
518
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.
519
540
  - `method` — Async handler. Receives merged call args + `buildContext` output.
520
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
+
521
548
  ### `registerToolFromSchema(server, params)`
522
549
 
523
550
  Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
package/dist/index.cjs CHANGED
@@ -44,15 +44,17 @@ var toolError = message => {
44
44
  * Every registered tool automatically:
45
45
  * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
46
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.
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.
53
55
  * Gates own their own error handling; `onError` covers the tool handler only.
54
56
  * 4. Merges `buildContext` output into the handler args.
55
- * 5. Wraps `null`/`undefined` results in a "Not found" error result.
57
+ * 5. Wraps `null`/`undefined` results in a configurable "Not found" error result.
56
58
  * 6. Calls `onError` on handler throw before rethrowing.
57
59
  *
58
60
  * @example
@@ -66,20 +68,36 @@ var toolError = message => {
66
68
  * });
67
69
  *
68
70
  * 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),
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),
74
83
  * });
75
84
  * ```
76
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
+ };
77
93
  var createGatedToolRegistrar = ({
78
94
  server,
79
95
  resolveIdentity = getIdentity,
80
96
  gates = [],
97
+ enforceScope = true,
81
98
  onError,
82
- buildContext
99
+ buildContext,
100
+ notFoundMessage = "Not found"
83
101
  }) => {
84
102
  const register = def => {
85
103
  server.registerTool(def.name, {
@@ -88,15 +106,23 @@ var createGatedToolRegistrar = ({
88
106
  }, async args => {
89
107
  const identity = resolveIdentity();
90
108
  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) : {};
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) : {};
94
120
  try {
95
121
  const result = await def.method({
96
122
  ...args,
97
123
  ...extra
98
124
  });
99
- if (result == null) return toolError("Not found");
125
+ if (result == null) return toolError(notFoundMessage);
100
126
  return {
101
127
  content: [{
102
128
  type: "text",
@@ -104,10 +130,7 @@ var createGatedToolRegistrar = ({
104
130
  }]
105
131
  };
106
132
  } catch (error) {
107
- if (onError) await onError(error, {
108
- handler: def.name,
109
- identity
110
- });
133
+ if (onError) await onError(error, ctx);
111
134
  throw error;
112
135
  }
113
136
  });
package/dist/index.d.cts CHANGED
@@ -682,12 +682,34 @@ type ToolIdentity = {
682
682
  userId: string;
683
683
  scopes?: string[];
684
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
+ };
685
700
  /** Definition for a single gated tool. */
686
701
  type GatedToolDef = {
687
702
  /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
688
703
  description: string; /** The single scope that must be present on the caller's token. */
689
704
  requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
690
- inputSchema: unknown; /** The tool handler. Receives merged call args + `buildContext` output. */
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. */
691
713
  method: (args: Record<string, unknown>) => Promise<unknown>;
692
714
  };
693
715
  /** Options for {@link createGatedToolRegistrar}. */
@@ -699,70 +721,46 @@ type CreateGatedToolRegistrarOptions = {
699
721
  */
700
722
  resolveIdentity?: () => ToolIdentity;
701
723
  /**
702
- * Additional authorization gates run after the scope check, in order.
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.
703
728
  * Throw to reject the call; return (or resolve) to continue.
704
729
  * Gates own their own error handling — `onError` covers the tool handler only.
705
730
  */
706
- gates?: Array<(identity: ToolIdentity) => void | Promise<void>>;
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;
707
738
  /**
708
739
  * Called when the tool **handler** throws. Use for error reporting/telemetry.
709
740
  * The error is always rethrown after this hook completes.
710
741
  * Note: scope-check failures and gate rejections do not trigger `onError`.
711
742
  */
712
- onError?: (error: unknown, ctx: {
713
- handler: string;
714
- identity: ToolIdentity;
715
- }) => void | Promise<void>;
743
+ onError?: (error: unknown, ctx: ToolCallContext) => void | Promise<void>;
716
744
  /**
717
745
  * 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).
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"`.
720
753
  */
721
- buildContext?: (identity: ToolIdentity) => Record<string, unknown>;
754
+ notFoundMessage?: string;
722
755
  };
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
756
  declare const createGatedToolRegistrar: ({
761
757
  server,
762
758
  resolveIdentity,
763
759
  gates,
760
+ enforceScope,
764
761
  onError,
765
- buildContext
762
+ buildContext,
763
+ notFoundMessage
766
764
  }: CreateGatedToolRegistrarOptions) => {
767
765
  register: (def: GatedToolDef) => void;
768
766
  };
@@ -1118,4 +1116,4 @@ interface RegisterToolFromSchemaParams {
1118
1116
  */
1119
1117
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1120
1118
  //#endregion
1121
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, 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
@@ -683,12 +683,34 @@ type ToolIdentity = {
683
683
  userId: string;
684
684
  scopes?: string[];
685
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
+ };
686
701
  /** Definition for a single gated tool. */
687
702
  type GatedToolDef = {
688
703
  /** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
689
704
  description: string; /** The single scope that must be present on the caller's token. */
690
705
  requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
691
- inputSchema: unknown; /** The tool handler. Receives merged call args + `buildContext` output. */
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. */
692
714
  method: (args: Record<string, unknown>) => Promise<unknown>;
693
715
  };
694
716
  /** Options for {@link createGatedToolRegistrar}. */
@@ -700,70 +722,46 @@ type CreateGatedToolRegistrarOptions = {
700
722
  */
701
723
  resolveIdentity?: () => ToolIdentity;
702
724
  /**
703
- * Additional authorization gates run after the scope check, in order.
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.
704
729
  * Throw to reject the call; return (or resolve) to continue.
705
730
  * Gates own their own error handling — `onError` covers the tool handler only.
706
731
  */
707
- gates?: Array<(identity: ToolIdentity) => void | Promise<void>>;
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;
708
739
  /**
709
740
  * Called when the tool **handler** throws. Use for error reporting/telemetry.
710
741
  * The error is always rethrown after this hook completes.
711
742
  * Note: scope-check failures and gate rejections do not trigger `onError`.
712
743
  */
713
- onError?: (error: unknown, ctx: {
714
- handler: string;
715
- identity: ToolIdentity;
716
- }) => void | Promise<void>;
744
+ onError?: (error: unknown, ctx: ToolCallContext) => void | Promise<void>;
717
745
  /**
718
746
  * 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).
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"`.
721
754
  */
722
- buildContext?: (identity: ToolIdentity) => Record<string, unknown>;
755
+ notFoundMessage?: string;
723
756
  };
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
757
  declare const createGatedToolRegistrar: ({
762
758
  server,
763
759
  resolveIdentity,
764
760
  gates,
761
+ enforceScope,
765
762
  onError,
766
- buildContext
763
+ buildContext,
764
+ notFoundMessage
767
765
  }: CreateGatedToolRegistrarOptions) => {
768
766
  register: (def: GatedToolDef) => void;
769
767
  };
@@ -1119,4 +1117,4 @@ interface RegisterToolFromSchemaParams {
1119
1117
  */
1120
1118
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1121
1119
  //#endregion
1122
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, 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
@@ -41,15 +41,17 @@ var toolError = message => {
41
41
  * Every registered tool automatically:
42
42
  * 1. Resolves the caller identity (default: `getIdentity()`). Returns an
43
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.
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.
50
52
  * Gates own their own error handling; `onError` covers the tool handler only.
51
53
  * 4. Merges `buildContext` output into the handler args.
52
- * 5. Wraps `null`/`undefined` results in a "Not found" error result.
54
+ * 5. Wraps `null`/`undefined` results in a configurable "Not found" error result.
53
55
  * 6. Calls `onError` on handler throw before rethrowing.
54
56
  *
55
57
  * @example
@@ -63,20 +65,36 @@ var toolError = message => {
63
65
  * });
64
66
  *
65
67
  * 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),
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),
71
80
  * });
72
81
  * ```
73
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
+ };
74
90
  var createGatedToolRegistrar = ({
75
91
  server,
76
92
  resolveIdentity = getIdentity,
77
93
  gates = [],
94
+ enforceScope = true,
78
95
  onError,
79
- buildContext
96
+ buildContext,
97
+ notFoundMessage = "Not found"
80
98
  }) => {
81
99
  const register = def => {
82
100
  server.registerTool(def.name, {
@@ -85,15 +103,23 @@ var createGatedToolRegistrar = ({
85
103
  }, async args => {
86
104
  const identity = resolveIdentity();
87
105
  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) : {};
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) : {};
91
117
  try {
92
118
  const result = await def.method({
93
119
  ...args,
94
120
  ...extra
95
121
  });
96
- if (result == null) return toolError("Not found");
122
+ if (result == null) return toolError(notFoundMessage);
97
123
  return {
98
124
  content: [{
99
125
  type: "text",
@@ -101,10 +127,7 @@ var createGatedToolRegistrar = ({
101
127
  }]
102
128
  };
103
129
  } catch (error) {
104
- if (onError) await onError(error, {
105
- handler: def.name,
106
- identity
107
- });
130
+ if (onError) await onError(error, ctx);
108
131
  throw error;
109
132
  }
110
133
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3",
38
- "@ttoss/auth-core": "^0.10.0",
39
38
  "@ttoss/http-server": "^0.7.1",
39
+ "@ttoss/auth-core": "^0.10.0",
40
40
  "@ttoss/http-server-auth": "^0.3.7"
41
41
  },
42
42
  "devDependencies": {