@ttoss/http-server-mcp 0.33.2 → 0.34.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/MIGRATIONS.md +71 -0
- package/README.md +199 -177
- package/dist/index.cjs +694 -161
- package/dist/index.d.cts +307 -152
- package/dist/index.d.mts +307 -152
- package/dist/index.mjs +691 -160
- package/package.json +2 -2
package/MIGRATIONS.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
|
+
## One `Tool` type: `registerTools` replaces `registerToolFromSchema` and `createGatedToolRegistrar`
|
|
4
|
+
|
|
5
|
+
A tool is now one value, `Tool`, and registering is one call, `registerTools`,
|
|
6
|
+
which can also [defer](./README.md#deferring-tools-for-large-tool-sets) them
|
|
7
|
+
behind `search` / `describe` / `call`. Gating became a function from `Tool` to
|
|
8
|
+
`Tool`, so a gated tool is deferred like any other.
|
|
9
|
+
|
|
10
|
+
**`registerToolFromSchema`** — wrap the params in a `tools` list:
|
|
11
|
+
|
|
12
|
+
```diff
|
|
13
|
+
-registerToolFromSchema(server, {
|
|
14
|
+
- name: 'get-project',
|
|
15
|
+
- description: 'Get a project',
|
|
16
|
+
- inputSchema,
|
|
17
|
+
- handler,
|
|
18
|
+
-});
|
|
19
|
+
+registerTools({
|
|
20
|
+
+ server,
|
|
21
|
+
+ tools: [{ name: 'get-project', description: 'Get a project', inputSchema, handler }],
|
|
22
|
+
+});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`description` is now required. `validateArguments` and `_meta` keep their
|
|
26
|
+
meaning, as fields of the tool.
|
|
27
|
+
|
|
28
|
+
**`createGatedToolRegistrar`** — the registrar options become gate options
|
|
29
|
+
(minus `server` and `notFoundMessage`), and each `register(def)` becomes a
|
|
30
|
+
gated `defineTool`:
|
|
31
|
+
|
|
32
|
+
```diff
|
|
33
|
+
-const { register } = createGatedToolRegistrar({ server, resolveIdentity, gates });
|
|
34
|
+
+const gate = createToolGate({ resolveIdentity, gates });
|
|
35
|
+
|
|
36
|
+
-register({
|
|
37
|
+
- name: 'list-campaigns',
|
|
38
|
+
- description: 'List campaigns.',
|
|
39
|
+
- requiredScope: 'campaigns:read',
|
|
40
|
+
- inputSchema: { limit: z.number().optional() },
|
|
41
|
+
- method: ({ limit }) => fetchCampaigns(limit),
|
|
42
|
+
-});
|
|
43
|
+
+registerTools({
|
|
44
|
+
+ server,
|
|
45
|
+
+ tools: [
|
|
46
|
+
+ gate({
|
|
47
|
+
+ requiredScope: 'campaigns:read',
|
|
48
|
+
+ tool: defineTool({
|
|
49
|
+
+ name: 'list-campaigns',
|
|
50
|
+
+ description: 'List campaigns.',
|
|
51
|
+
+ inputSchema: z.object({ limit: z.number().optional() }),
|
|
52
|
+
+ method: ({ limit }) => fetchCampaigns(limit),
|
|
53
|
+
+ }),
|
|
54
|
+
+ }),
|
|
55
|
+
+ ],
|
|
56
|
+
+});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **`inputSchema` and `outputSchema` take `z.object({...})`**, not a Zod field
|
|
60
|
+
map: a schema is now a JSON Schema or a Standard Schema. Wrap
|
|
61
|
+
`{ limit: z.number() }` as `z.object({ limit: z.number() })`.
|
|
62
|
+
- **`notFoundMessage` moved to `defineTool`**, per tool. A registrar-wide value
|
|
63
|
+
is now a wrapper around `defineTool` that sets it.
|
|
64
|
+
- **Per-tool `gates` move to the gate call**, next to `requiredScope`.
|
|
65
|
+
- `GatedToolDef` and `CreateGatedToolRegistrarOptions` are gone;
|
|
66
|
+
`CreateToolGateOptions`, `ToolCallGate`, `ToolCallContext` and
|
|
67
|
+
`ToolIdentity` remain.
|
|
68
|
+
|
|
69
|
+
**What you will observe if you miss this.** The build fails: every removed
|
|
70
|
+
export is a missing import, and a Zod field map passed as `inputSchema` is a
|
|
71
|
+
type error. Nothing changes at runtime for code that compiles — results,
|
|
72
|
+
scope refusals, gate order and localized errors are byte-for-byte the same.
|
|
73
|
+
|
|
3
74
|
## The `2026-07-28` revision needs `createMcpServer`
|
|
4
75
|
|
|
5
76
|
That revision is served only when `createMcpRouter` is given a
|
package/README.md
CHANGED
|
@@ -12,7 +12,12 @@ pnpm add @ttoss/http-server-mcp
|
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { App, bodyParser, cors } from '@ttoss/http-server';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createMcpRouter,
|
|
17
|
+
McpServer,
|
|
18
|
+
registerTools,
|
|
19
|
+
z,
|
|
20
|
+
} from '@ttoss/http-server-mcp';
|
|
16
21
|
|
|
17
22
|
// Create MCP server
|
|
18
23
|
const mcpServer = new McpServer({
|
|
@@ -21,23 +26,21 @@ const mcpServer = new McpServer({
|
|
|
21
26
|
});
|
|
22
27
|
|
|
23
28
|
// Register tools
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
registerTools({
|
|
30
|
+
server: mcpServer,
|
|
31
|
+
tools: [
|
|
32
|
+
{
|
|
33
|
+
name: 'get-weather',
|
|
34
|
+
description: 'Get weather information for a location',
|
|
35
|
+
inputSchema: z.object({ location: z.string().describe('City name') }),
|
|
36
|
+
handler: async ({ location }) => ({
|
|
37
|
+
content: [
|
|
38
|
+
{ type: 'text', text: `Weather in ${location}: Sunny, 72°F` },
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
30
41
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
content: [
|
|
34
|
-
{
|
|
35
|
-
type: 'text',
|
|
36
|
-
text: `Weather in ${location}: Sunny, 72°F`,
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
})
|
|
40
|
-
);
|
|
42
|
+
],
|
|
43
|
+
});
|
|
41
44
|
|
|
42
45
|
// Create HTTP server
|
|
43
46
|
const app = new App();
|
|
@@ -435,29 +438,83 @@ createMcpRouter(mcpServer, {
|
|
|
435
438
|
|
|
436
439
|
The discovery endpoints (both locations above) remain publicly accessible even when `aliases` includes `'/'`. Aliases themselves get no derived location — the metadata `resource` always names the primary `path`.
|
|
437
440
|
|
|
438
|
-
##
|
|
441
|
+
## Tools
|
|
442
|
+
|
|
443
|
+
A `Tool` is what a tool is and how it answers, independent of how a server exposes it: `name`, `description`, `inputSchema`, `handler`, and optionally `title`, `outputSchema`, `annotations`, `tags`, `summary` and `_meta`. `registerTools` registers a list of them.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { defineTool, registerTools, z } from '@ttoss/http-server-mcp';
|
|
447
|
+
|
|
448
|
+
registerTools({
|
|
449
|
+
server,
|
|
450
|
+
tools: [
|
|
451
|
+
{
|
|
452
|
+
name: 'get-project',
|
|
453
|
+
description: 'Get a project by ID',
|
|
454
|
+
inputSchema: z.object({ id: z.string() }),
|
|
455
|
+
handler: async ({ id }) => ({
|
|
456
|
+
content: [{ type: 'text', text: `Project: ${id}` }],
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
// defineTool: answer with data; null answers "Not found" as an error.
|
|
460
|
+
defineTool({
|
|
461
|
+
name: 'list-campaigns',
|
|
462
|
+
description: 'List the caller campaigns.',
|
|
463
|
+
inputSchema: z.object({ limit: z.number().optional() }),
|
|
464
|
+
method: ({ limit }) => fetchCampaigns(limit),
|
|
465
|
+
}),
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`inputSchema` is either a Standard Schema (Zod 4's `z.object(...)`), always enforced, with the handler receiving its parsed output; or a plain JSON Schema, forwarded verbatim and enforced only with `validateArguments` — see [Argument validation](#argument-validation). A plain JSON Schema keeps one definition shareable with an AI SDK agent, whose `tool()` helper takes the same object.
|
|
471
|
+
|
|
472
|
+
### Deferring tools for large tool sets
|
|
473
|
+
|
|
474
|
+
Every tool definition ships to the model on every turn, so a few hundred tools overflow a client's context, and some providers cap the number of tools per request. `defer` registers the same tools behind three that load them on demand:
|
|
439
475
|
|
|
440
|
-
|
|
476
|
+
| Tool | Input | Returns |
|
|
477
|
+
| ---------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
478
|
+
| `search` | `query`, `tag?`, `limit?` | Name, one-line `summary` and `tags` of the best matches |
|
|
479
|
+
| `describe` | `names` | Each tool's full definition, including `inputSchema`; unknown names come back with suggestions |
|
|
480
|
+
| `call` | `name`, `arguments` | Exactly what the tool answers |
|
|
441
481
|
|
|
442
|
-
|
|
482
|
+
```typescript
|
|
483
|
+
registerTools({ server: fullServer, tools });
|
|
484
|
+
registerTools({ server: deferredServer, tools, defer: true });
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
A schema enters the model's context only for the tools it is about to use. `call` enforces the tool's schema as a direct call would, and its errors help the model recover: an unknown name answers suggestions, and invalid arguments, a tool's own error result, or a thrown error carry the input schema, so a `call` made without `describe` corrects itself in one retry.
|
|
488
|
+
|
|
489
|
+
Pass options instead of `true` to tune it:
|
|
490
|
+
|
|
491
|
+
- `except` — tools kept standalone, not deferred; they stay reachable through `search` too. Defaults to those linked to an [MCP Apps](#mcp-apps-interactive-uis) view, since a host finds a view through the tool's own `tools/list` entry.
|
|
492
|
+
- `visible` — runs on every `search`, `describe` and `call`; a tool it rejects is absent from all three. Standalone tools stay listed.
|
|
493
|
+
- `search` — replaces the default ranking, `rankTools`, e.g. with embeddings.
|
|
494
|
+
- `searchLimit` (default `10`) and `names` (default `search` / `describe` / `call`).
|
|
495
|
+
|
|
496
|
+
A gate (below) sees each call with the tool's own name and arguments, so authorization stays per tool when deferred.
|
|
497
|
+
|
|
498
|
+
### Gating tools
|
|
499
|
+
|
|
500
|
+
`createToolGate` returns a gate: a function that wraps a `Tool` so every call passes the same authentication and authorization pipeline before its handler runs. The gated tool is an ordinary `Tool`.
|
|
443
501
|
|
|
444
502
|
1. Resolves the caller's identity (defaults to `getIdentity()` from the request context).
|
|
445
|
-
2. Checks that the caller holds
|
|
446
|
-
3. Runs global `gates` then
|
|
503
|
+
2. Checks that the caller holds the tool's `requiredScope` — returns an `isError` result (not a throw) when the scope is absent.
|
|
504
|
+
3. Runs global `gates` then the tool's `gates` in order; a throwing gate rejects the call. Both receive `ToolCallContext` (identity **and** the validated call args).
|
|
447
505
|
4. Merges `buildContext` output into the handler args.
|
|
448
|
-
5.
|
|
449
|
-
6. Calls `onError` on handler throw before rethrowing, for telemetry. Gate/scope failures do **not** trigger `onError`.
|
|
506
|
+
5. Calls `onError` on handler throw before rethrowing, for telemetry. Gate/scope failures do **not** trigger `onError`.
|
|
450
507
|
|
|
451
508
|
```typescript
|
|
452
509
|
import {
|
|
453
|
-
|
|
510
|
+
createToolGate,
|
|
511
|
+
defineTool,
|
|
454
512
|
getIdentity,
|
|
455
|
-
|
|
513
|
+
registerTools,
|
|
514
|
+
z,
|
|
456
515
|
} from '@ttoss/http-server-mcp';
|
|
457
|
-
import { z } from 'zod';
|
|
458
516
|
|
|
459
|
-
const
|
|
460
|
-
server,
|
|
517
|
+
const gate = createToolGate({
|
|
461
518
|
resolveIdentity: () => {
|
|
462
519
|
const jwt = getIdentity<{ sub: string; scope: string }>();
|
|
463
520
|
const scopes = jwt?.scope?.split(' ') ?? [];
|
|
@@ -469,47 +526,39 @@ const { register } = createGatedToolRegistrar({
|
|
|
469
526
|
},
|
|
470
527
|
});
|
|
471
528
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
args.isActive
|
|
496
|
-
? checkSubscriptionGates(['mustNotExceedMaxActive', 'mustHaveBudget'])
|
|
497
|
-
: checkSubscriptionGates(['mustIncludeService']),
|
|
529
|
+
registerTools({
|
|
530
|
+
server,
|
|
531
|
+
tools: [
|
|
532
|
+
gate({
|
|
533
|
+
requiredScope: 'accounts:write',
|
|
534
|
+
// arg-conditional gate: activating requires more checks than deactivating
|
|
535
|
+
gates: [
|
|
536
|
+
({ args }) =>
|
|
537
|
+
args.isActive
|
|
538
|
+
? checkSubscriptionGates([
|
|
539
|
+
'mustNotExceedMaxActive',
|
|
540
|
+
'mustHaveBudget',
|
|
541
|
+
])
|
|
542
|
+
: checkSubscriptionGates(['mustIncludeService']),
|
|
543
|
+
],
|
|
544
|
+
tool: defineTool({
|
|
545
|
+
name: 'activate-ad-account',
|
|
546
|
+
description: 'Activate or deactivate an ad account.',
|
|
547
|
+
inputSchema: z.object({ accountId: z.number(), isActive: z.boolean() }),
|
|
548
|
+
method: ({ tenantId, accountId, isActive }) =>
|
|
549
|
+
toggleAdAccount(tenantId, accountId, isActive),
|
|
550
|
+
}),
|
|
551
|
+
}),
|
|
498
552
|
],
|
|
499
|
-
method: async ({ userId, accountId, isActive }) =>
|
|
500
|
-
toggleAdAccount(userId, accountId, isActive),
|
|
501
553
|
});
|
|
502
554
|
```
|
|
503
555
|
|
|
504
|
-
**`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.
|
|
505
|
-
|
|
506
556
|
### Localized refusals and errors
|
|
507
557
|
|
|
508
|
-
Pass `i18n` to render a [`LocalizedError`](https://ttoss.dev/docs/modules/packages/i18n-core/) thrown by a **gate** or by the **handler** as an `isError` result `{ "error": "<message>", "code": "<CODE>" }` in the caller's locale. Gates run outside the handler, so the
|
|
558
|
+
Pass `i18n` to render a [`LocalizedError`](https://ttoss.dev/docs/modules/packages/i18n-core/) thrown by a **gate** or by the **handler** as an `isError` result `{ "error": "<message>", "code": "<CODE>" }` in the caller's locale. Gates run outside the handler, so the gate is the one place that covers both.
|
|
509
559
|
|
|
510
560
|
```ts
|
|
511
|
-
const
|
|
512
|
-
server,
|
|
561
|
+
const gate = createToolGate({
|
|
513
562
|
gates: [subscriptionGate],
|
|
514
563
|
i18n: {
|
|
515
564
|
catalog,
|
|
@@ -558,36 +607,36 @@ const dashboard = registerAppResource({
|
|
|
558
607
|
},
|
|
559
608
|
});
|
|
560
609
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
_meta: dashboard.toolMeta({ visibility: ['app'] }),
|
|
587
|
-
handler: async () => ({ content: [{ type: 'text', text: 'refreshed' }] }),
|
|
610
|
+
registerTools({
|
|
611
|
+
server,
|
|
612
|
+
tools: [
|
|
613
|
+
{
|
|
614
|
+
name: 'get-weather',
|
|
615
|
+
description: 'Get the weather for a location',
|
|
616
|
+
inputSchema: z.object({ location: z.string() }),
|
|
617
|
+
_meta: dashboard.toolMeta(),
|
|
618
|
+
handler: async ({ location }) => {
|
|
619
|
+
const forecast = await fetchForecast(location);
|
|
620
|
+
return {
|
|
621
|
+
// Text stays meaningful: it is what a host without Apps renders.
|
|
622
|
+
content: [{ type: 'text', text: summarise(forecast) }],
|
|
623
|
+
structuredContent: forecast,
|
|
624
|
+
};
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: 'refresh-dashboard',
|
|
629
|
+
description: 'Refresh dashboard data',
|
|
630
|
+
// Callable by the view, hidden from the model.
|
|
631
|
+
_meta: dashboard.toolMeta({ visibility: ['app'] }),
|
|
632
|
+
handler: async () => ({ content: [{ type: 'text', text: 'refreshed' }] }),
|
|
633
|
+
},
|
|
634
|
+
],
|
|
588
635
|
});
|
|
589
636
|
```
|
|
590
637
|
|
|
638
|
+
`toolMeta()` is the linkage for every registration path: a `Tool`'s `_meta`, or the SDK's `registerTool`. When [deferred](#deferring-tools-for-large-tool-sets), view-linked tools stay registered standalone by default.
|
|
639
|
+
|
|
591
640
|
### Register the linkage unconditionally
|
|
592
641
|
|
|
593
642
|
The spec suggests checking the client's declared capability before registering UI-enabled tools. Do not: that read is only reliable on the `2026-07-28` revision, where the capability arrives in each request's envelope. In this package's default [stateless mode](#stateless-vs-stateful-mode) there is no remembered `initialize` for a 2025-era client, so a capability-gated registration silently drops the view for every one of them.
|
|
@@ -705,41 +754,62 @@ Returns the `WWW-Authenticate` header value for a 401 response, formatted per th
|
|
|
705
754
|
|
|
706
755
|
**Returns:** `string` — The full `WWW-Authenticate` header value
|
|
707
756
|
|
|
708
|
-
### `
|
|
757
|
+
### `registerTools(params)`
|
|
709
758
|
|
|
710
|
-
|
|
759
|
+
Registers `Tool`s on an MCP server. See [Tools](#tools).
|
|
711
760
|
|
|
712
|
-
**Parameters (`
|
|
761
|
+
**Parameters (`params`):**
|
|
713
762
|
|
|
714
|
-
- `server` (`McpServer`) — The MCP server to register
|
|
715
|
-
- `
|
|
716
|
-
- `
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
- `
|
|
763
|
+
- `server` (`McpServer`) — The MCP server to register on.
|
|
764
|
+
- `tools` (`Tool[]`) — The tools.
|
|
765
|
+
- `defer` (`boolean | DeferToolsOptions`, optional, default `false`) — Defer the tools behind `search` / `describe` / `call`; see [Deferring tools for large tool sets](#deferring-tools-for-large-tool-sets) for the options.
|
|
766
|
+
|
|
767
|
+
**`Tool`:**
|
|
768
|
+
|
|
769
|
+
- `name` (`string`) — Unique tool name.
|
|
770
|
+
- `description` (`string`) — What the tool does, written for the model.
|
|
771
|
+
- `inputSchema` (`JsonObjectSchema | StandardSchemaWithJSON`, optional) — Defaults to `{ type: 'object', properties: {} }`.
|
|
772
|
+
- `validateArguments` (`boolean`, optional, default `false`) — Enforce a JSON Schema `inputSchema`; see [Argument validation](#argument-validation).
|
|
773
|
+
- `outputSchema` (same types as `inputSchema`, optional) — Advertised on `tools/list`; `structuredContent` is validated against it.
|
|
774
|
+
- `title`, `annotations`, `_meta` (optional) — Forwarded on `tools/list`.
|
|
775
|
+
- `tags`, `summary` (optional) — Read by the deferred `search`; never sent on `tools/list`.
|
|
776
|
+
- `handler` (`(args) => CallToolResult | Promise<CallToolResult>`) — Answers a call.
|
|
777
|
+
|
|
778
|
+
**Returns:** `void`
|
|
779
|
+
|
|
780
|
+
#### Argument validation
|
|
781
|
+
|
|
782
|
+
By default, a JSON Schema `inputSchema` is **advertised but not enforced**: it round-trips verbatim over `tools/list` so clients know what to send, while arguments reach your handler unchecked. Set `validateArguments: true` to reject mismatched calls before the handler runs.
|
|
721
783
|
|
|
722
|
-
|
|
784
|
+
Enable it once you're confident `inputSchema` describes every value the tool genuinely accepts. Schemas derived from an OpenAPI document are a common source of _incomplete_ ones — a field a client may send as `null` to clear it, or one that accepts several shapes, is easily emitted as a bare `{ type: 'string' }`. Validating against a schema like that rejects calls the underlying API would have accepted. To describe those cases accurately, use `{ type: ['string', 'null'] }` for a nullable field and forward `oneOf`/`anyOf` verbatim rather than collapsing to one type.
|
|
785
|
+
|
|
786
|
+
### `defineTool(params)`
|
|
787
|
+
|
|
788
|
+
Builds a `Tool` from a `method` that returns data: JSON text, plus `structuredContent` when `outputSchema` is set. `null`/`undefined` answers `notFoundMessage` (default `"Not found"`) as an `isError` result. Takes every `Tool` field but `handler`.
|
|
723
789
|
|
|
724
|
-
|
|
790
|
+
### `createToolGate(options?)`
|
|
725
791
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
- `
|
|
731
|
-
- `
|
|
732
|
-
- `
|
|
733
|
-
- `
|
|
734
|
-
- `
|
|
735
|
-
- `
|
|
792
|
+
Returns a gate, `({ tool, requiredScope, gates? }) => Tool`, that wraps a tool in the pipeline described in [Gating tools](#gating-tools).
|
|
793
|
+
|
|
794
|
+
**Parameters (`options`):**
|
|
795
|
+
|
|
796
|
+
- `resolveIdentity` (`() => ToolIdentity`, optional) — Called once per invocation to resolve `{ userId, scopes? }`. Defaults to `getIdentity()` from the request context.
|
|
797
|
+
- `gates` (`ToolCallGate[]`, 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.
|
|
798
|
+
- `enforceScope` (`boolean`, optional, default `true`) — When `true`, checks `requiredScope` against `identity.scopes` and returns an `isError` result on mismatch. Set to `false` when all authorization is handled by `gates`.
|
|
799
|
+
- `buildContext` (`(ctx: ToolCallContext) => Record<string, unknown>`, optional) — Produces extra key-value pairs merged into every handler's args.
|
|
800
|
+
- `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.
|
|
801
|
+
- `i18n` (optional) — See [Localized refusals and errors](#localized-refusals-and-errors).
|
|
736
802
|
|
|
737
803
|
**`ToolCallContext`** is the object passed to gates, `buildContext`, and `onError`:
|
|
738
804
|
|
|
739
805
|
- `identity` (`ToolIdentity`) — The resolved caller identity (`{ userId, scopes? }`).
|
|
740
|
-
- `args` (`Record<string, unknown>`) — The validated tool input
|
|
806
|
+
- `args` (`Record<string, unknown>`) — The validated tool input.
|
|
741
807
|
- `handler` (`string`) — The tool name, for error attribution.
|
|
742
808
|
|
|
809
|
+
### `rankTools(args)`
|
|
810
|
+
|
|
811
|
+
The deferred `search`'s default ranking, exported as a fallback for a custom one: scores `tools` by the terms of `query` — weighted name > summary > tags > description, plural and singular alike, a word's prefix at half weight — filters by `tag`, and returns up to `limit`, ties in the tools' order.
|
|
812
|
+
|
|
743
813
|
### `registerAppResource(params)`
|
|
744
814
|
|
|
745
815
|
Registers an [MCP Apps](#mcp-apps-interactive-uis) view: a `ui://` resource whose HTML a host renders for a linked tool's result.
|
|
@@ -767,65 +837,11 @@ Registers an [MCP Apps](#mcp-apps-interactive-uis) view: a `ui://` resource whos
|
|
|
767
837
|
|
|
768
838
|
`'io.modelcontextprotocol/ui'` and `'text/html;profile=mcp-app'`. Use the first as the `capabilities.extensions` key when advertising Apps support on the `McpServer`, and the second as the MIME type a host negotiates.
|
|
769
839
|
|
|
770
|
-
### `registerToolFromSchema(server, params)`
|
|
771
|
-
|
|
772
|
-
Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
|
|
773
|
-
|
|
774
|
-
Use this when tool definitions are shared between the MCP server and an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper). Both consumers accept plain JSON Schema at runtime, so a single definition can feed both without any lossy conversion.
|
|
775
|
-
|
|
776
|
-
**Parameters:**
|
|
777
|
-
|
|
778
|
-
- `server` (`McpServer`) — The MCP server instance
|
|
779
|
-
- `params.name` (`string`) — Unique tool name
|
|
780
|
-
- `params.description` (`string`, optional) — Human-readable description
|
|
781
|
-
- `params.inputSchema` (`JsonObjectSchema`, optional) — Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
|
|
782
|
-
- `params.validateArguments` (`boolean`, optional) — Enforce `inputSchema` on `tools/call` (default: `false`); see [Argument validation](#argument-validation)
|
|
783
|
-
- `params._meta` (`Record<string, unknown>`, optional) — Tool metadata forwarded verbatim on `tools/list`; how a tool links to an [MCP Apps](#mcp-apps-interactive-uis) view
|
|
784
|
-
- `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) — Tool handler receiving the raw request arguments
|
|
785
|
-
|
|
786
|
-
**Returns:** `void`
|
|
787
|
-
|
|
788
|
-
#### Argument validation
|
|
789
|
-
|
|
790
|
-
By default, `inputSchema` is **advertised but not enforced**: it round-trips verbatim over `tools/list` so clients know what to send, while arguments reach your handler unchecked. Pass `validateArguments: true` to reject mismatched calls with an MCP error before the handler runs.
|
|
791
|
-
|
|
792
|
-
Enable it once you're confident `inputSchema` describes every value the tool genuinely accepts. Schemas derived from an OpenAPI document are a common source of _incomplete_ ones — a field a client may send as `null` to clear it, or one that accepts several shapes, is easily emitted as a bare `{ type: 'string' }`. Validating against a schema like that rejects calls the underlying API would have accepted. To describe those cases accurately, use `{ type: ['string', 'null'] }` for a nullable field and forward `oneOf`/`anyOf` verbatim rather than collapsing to one type.
|
|
793
|
-
|
|
794
840
|
## Examples
|
|
795
841
|
|
|
796
|
-
###
|
|
797
|
-
|
|
798
|
-
Use `registerToolFromSchema` when you share tool definitions across the MCP server **and** an AI SDK agent. The plain JSON Schema is forwarded verbatim over the MCP wire protocol — `anyOf`, `$ref`, `pattern`, and other features not supported by Zod v3 are preserved without loss.
|
|
799
|
-
|
|
800
|
-
```typescript
|
|
801
|
-
import {
|
|
802
|
-
createMcpRouter,
|
|
803
|
-
McpServer,
|
|
804
|
-
registerToolFromSchema,
|
|
805
|
-
} from '@ttoss/http-server-mcp';
|
|
806
|
-
|
|
807
|
-
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
808
|
-
|
|
809
|
-
registerToolFromSchema(server, {
|
|
810
|
-
name: 'get-project',
|
|
811
|
-
description: 'Get a project by ID',
|
|
812
|
-
inputSchema: {
|
|
813
|
-
type: 'object',
|
|
814
|
-
properties: {
|
|
815
|
-
id: { type: 'string', description: 'Project public ID' },
|
|
816
|
-
// anyOf is preserved — Zod v3 has no direct equivalent
|
|
817
|
-
status: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
|
818
|
-
},
|
|
819
|
-
required: ['id'],
|
|
820
|
-
},
|
|
821
|
-
handler: async ({ id }) => {
|
|
822
|
-
const data = await apiCall('GET', `/projects/${id}`);
|
|
823
|
-
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
824
|
-
},
|
|
825
|
-
});
|
|
826
|
-
```
|
|
842
|
+
### Single source of truth across MCP and AI SDK
|
|
827
843
|
|
|
828
|
-
|
|
844
|
+
A JSON Schema `inputSchema` is forwarded verbatim — `anyOf`, `$ref`, `pattern` survive — so one definition feeds both:
|
|
829
845
|
|
|
830
846
|
```typescript
|
|
831
847
|
// lib/tools.ts — shared tool definition
|
|
@@ -834,23 +850,29 @@ export const getProjectTool = {
|
|
|
834
850
|
description: 'Get a project by ID',
|
|
835
851
|
inputSchema: {
|
|
836
852
|
type: 'object' as const,
|
|
837
|
-
properties: {
|
|
853
|
+
properties: {
|
|
854
|
+
id: { type: 'string' },
|
|
855
|
+
status: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
|
856
|
+
},
|
|
838
857
|
required: ['id'],
|
|
839
858
|
},
|
|
840
859
|
};
|
|
841
860
|
|
|
842
861
|
// MCP server
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
862
|
+
registerTools({
|
|
863
|
+
server: mcpServer,
|
|
864
|
+
tools: [
|
|
865
|
+
{
|
|
866
|
+
...getProjectTool,
|
|
867
|
+
handler: async ({ id }) => {
|
|
868
|
+
/* ... */
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
],
|
|
849
872
|
});
|
|
850
873
|
|
|
851
874
|
// AI SDK agent
|
|
852
|
-
import { tool } from 'ai';
|
|
853
|
-
import { jsonSchema } from 'ai';
|
|
875
|
+
import { jsonSchema, tool } from 'ai';
|
|
854
876
|
const agentTool = tool({
|
|
855
877
|
description: getProjectTool.description,
|
|
856
878
|
parameters: jsonSchema(getProjectTool.inputSchema),
|