@ttoss/http-server-mcp 0.20.0 → 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 +36 -9
- package/dist/index.cjs +44 -21
- package/dist/index.d.cts +47 -49
- package/dist/index.d.mts +47 -49
- package/dist/index.mjs +44 -21
- package/package.json +2 -2
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
|
|
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: ({
|
|
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.
|
|
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
|
-
|
|
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<(
|
|
508
|
-
- `
|
|
509
|
-
- `
|
|
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.
|
|
48
|
-
* the scope is absent, consistent with how MCP
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 3. Runs
|
|
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: '
|
|
70
|
-
* description: '
|
|
71
|
-
* requiredScope: 'campaigns:
|
|
72
|
-
* inputSchema: {
|
|
73
|
-
*
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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;
|
|
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
|
-
*
|
|
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<(
|
|
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.
|
|
719
|
-
*
|
|
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
|
-
|
|
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;
|
|
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
|
-
*
|
|
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<(
|
|
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.
|
|
720
|
-
*
|
|
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
|
-
|
|
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.
|
|
45
|
-
* the scope is absent, consistent with how MCP
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* 3. Runs
|
|
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: '
|
|
67
|
-
* description: '
|
|
68
|
-
* requiredScope: 'campaigns:
|
|
69
|
-
* inputSchema: {
|
|
70
|
-
*
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.21.0",
|
|
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": {
|