@george43g/mcp-kit 2.0.0 → 3.1.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 +215 -2
- package/dist/dispatch.d.ts +10 -2
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +82 -3
- package/dist/dispatch.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +89 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +230 -0
- package/dist/testing.js.map +1 -0
- package/dist/tool-registry.d.ts +196 -7
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/tool-registry.js +103 -17
- package/dist/tool-registry.js.map +1 -1
- package/dist/unknown-args.d.ts +39 -0
- package/dist/unknown-args.d.ts.map +1 -0
- package/dist/unknown-args.js +96 -0
- package/dist/unknown-args.js.map +1 -0
- package/package.json +13 -5
- package/src/dispatch.ts +109 -5
- package/src/index.ts +11 -1
- package/src/testing.ts +303 -0
- package/src/tool-registry.ts +287 -25
- package/src/unknown-args.ts +102 -0
package/README.md
CHANGED
|
@@ -4,8 +4,167 @@ MCP server building blocks: a tool registry, a dispatcher with timeout / perf /
|
|
|
4
4
|
abort / error-wrapping baked in, stdio + Streamable HTTP transports, `sanitize()`
|
|
5
5
|
for untrusted content, and UUID-gated prompt-injection helpers.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Peers: `zod` ^4 and `@george43g/robustness`. Upgrading from 2.x? See
|
|
8
|
+
[Migrating from 2.x](#migrating-from-2x-to-300) — the wire schemas changed dialect.
|
|
9
|
+
|
|
10
|
+
## Schemas: one Zod definition, JSON Schema 2020-12 on the wire
|
|
11
|
+
|
|
12
|
+
Each tool declares a Zod `input` and `output`. `makeRegistry()` derives both
|
|
13
|
+
wire schemas from them with `toMcpSchema()`; there is no hand-written JSON
|
|
14
|
+
Schema path, so the schema a client sees cannot drift from the validator the
|
|
15
|
+
dispatcher runs.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { toMcpSchema } from "@george43g/mcp-kit";
|
|
19
|
+
|
|
20
|
+
toMcpSchema(NoopInputSchema, "input", "noop");
|
|
21
|
+
// → { type: "object", properties: { … }, required: [ … ] } (no "$schema")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Dialect 2020-12, `$schema` stripped.** 2020-12 is MCP's default dialect,
|
|
25
|
+
and a schema with no label gives no client a dialect to refuse. mcp-kit 2.x
|
|
26
|
+
emitted draft-07 via `zod-to-json-schema`, and Claude Code 2.1.292 throws on
|
|
27
|
+
`"$schema": "http://json-schema.org/draft-07/schema#"` — every 2.x tool broke.
|
|
28
|
+
- **`io` is real.** `inputSchema` describes what a caller may send (a
|
|
29
|
+
`.default()` field is optional); `outputSchema` describes what parsing yields
|
|
30
|
+
(that field is required, and objects carry `additionalProperties: false`).
|
|
31
|
+
- **Unrepresentable types throw at `makeRegistry()`**, naming the tool:
|
|
32
|
+
`z.date()`, `z.bigint()`, an output-side `.transform()`. Use an ISO string, a
|
|
33
|
+
number, or move the transform out of the schema.
|
|
34
|
+
- **The top level must be a `z.object`.** MCP requires `type: "object"`.
|
|
35
|
+
- **Recursive schemas keep `$defs` / `$ref`** (or a root `$ref: "#"`): a cycle
|
|
36
|
+
cannot be inlined. That is valid 2020-12, and the conformance helper below
|
|
37
|
+
compiles it.
|
|
38
|
+
|
|
39
|
+
## Per-request tool filter (scope gating)
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const registry = makeRegistry(tools, {
|
|
43
|
+
filter: (tool, ctx) =>
|
|
44
|
+
!tool.name.startsWith("gmail_send") || (ctx.authInfo?.scopes.includes("gmail.send") ?? false),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.setRequestHandler(ListToolsRequestSchema, async (_req, extra) => ({
|
|
48
|
+
tools: registry.toMcpTools(includeDevOnly, extra),
|
|
49
|
+
}));
|
|
50
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) =>
|
|
51
|
+
dispatch(req.params.name, req.params.arguments ?? {}, extra.signal, extra),
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- The filter lives on the **registry**, so the listing and the dispatcher apply
|
|
56
|
+
the same predicate. A refused call answers **exactly like an unknown tool**.
|
|
57
|
+
Hiding a tool from `tools/list` while it still answers by name is the hole
|
|
58
|
+
`devOnly` fell into once.
|
|
59
|
+
- `ctx` is `{ authInfo?, sessionId?, requestInfo? }` — a subset of the SDK's
|
|
60
|
+
`RequestHandlerExtra`, so pass `extra` straight through. In-process calls (a
|
|
61
|
+
CLI, a test) pass nothing and the filter sees `{}`; decide what no `authInfo`
|
|
62
|
+
means for your tools.
|
|
63
|
+
- Synchronous, evaluated per request. **A throwing filter counts as `false`**.
|
|
64
|
+
|
|
65
|
+
### Refusing with a reason: `{ deny }` (3.1.0)
|
|
66
|
+
|
|
67
|
+
`false` makes a tool look like it does not exist, which is right when its
|
|
68
|
+
existence is the secret and a dead end when the caller could fix the problem.
|
|
69
|
+
Return `{ deny: "reason" }` instead and the tool is hidden from `tools/list`
|
|
70
|
+
the same way, but a `tools/call` to it gets the reason:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const registry = makeRegistry(tools, {
|
|
74
|
+
filter: (tool, ctx) =>
|
|
75
|
+
hasScopes(ctx, tool.scopes) ||
|
|
76
|
+
{ deny: "not available. You may need to re-authenticate with additional scopes" },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await dispatch("gmail_send", { to: "…" });
|
|
80
|
+
// → { isError: true, content: [{ type: "text", text:
|
|
81
|
+
// 'Tool "gmail_send" failed: not available. You may need to re-authenticate with additional scopes' }] }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- The reason is wrapped like every other dispatcher error
|
|
85
|
+
(`Tool "<name>" failed: <deny>`). Write it as a sentence an agent can act on.
|
|
86
|
+
- `false` keeps the unknown-tool wording, a throw still counts as `false`, and
|
|
87
|
+
so does `{ deny: "" }`: an empty reason tells the caller nothing.
|
|
88
|
+
- A gated `devOnly` tool answers as unknown even if the filter would deny it
|
|
89
|
+
with a reason; the filter is not consulted for it.
|
|
90
|
+
- `registry.allows()` still returns a boolean (`false` for `{ deny }`).
|
|
91
|
+
`registry.evaluate(def, ctx)` returns `{ allowed: true }` or
|
|
92
|
+
`{ allowed: false, deny? }`. It is optional on the `ToolRegistry` type, so a
|
|
93
|
+
hand-written 3.0 registry still compiles; the dispatcher falls back to
|
|
94
|
+
`allows()` for one, and its refusals then use the unknown-tool wording.
|
|
95
|
+
|
|
96
|
+
## Strict arguments: `strictArgs` (3.1.0, off by default)
|
|
97
|
+
|
|
98
|
+
Zod strips an argument the schema does not declare. So when an agent misspells
|
|
99
|
+
an OPTIONAL argument, the call succeeds and the handler runs on the default as
|
|
100
|
+
if the argument had never been sent. Nothing tells the agent it was ignored.
|
|
101
|
+
`strictArgs` refuses the call instead, before parsing and before the handler:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const registry = makeRegistry(tools, { strictArgs: true }); // every tool
|
|
105
|
+
const chat: ToolDefinition = { /* … */ strictArgs: false }; // a tool opts back out
|
|
106
|
+
|
|
107
|
+
await dispatch("chat_history", { chatIdentifer: "x", cursor: "c" });
|
|
108
|
+
// Tool "chat_history" failed: unknown argument chatIdentifer; did you mean chatIdentifier?
|
|
109
|
+
// unknown argument cursor
|
|
110
|
+
// Hint: Valid arguments: chatIdentifier, limit.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **Set it on the registry, override it per tool.** `ToolDefinition.strictArgs`
|
|
114
|
+
wins over `makeRegistry`'s `strictArgs` in either direction.
|
|
115
|
+
- **"Unknown" means "not in the advertised `inputSchema`'s top-level
|
|
116
|
+
`properties`."** So it behaves the same for a refined or transformed
|
|
117
|
+
`z.object` and for an intersection, which Zod merges into one object.
|
|
118
|
+
- **The advertised `inputSchema` does not change.** No `additionalProperties:
|
|
119
|
+
false` is added; clients see the same open schema whether this is on or off.
|
|
120
|
+
- **Valid input parses exactly as before.** The check only compares argument
|
|
121
|
+
names; parsing still runs the tool's own schema, so defaults and transforms
|
|
122
|
+
are untouched.
|
|
123
|
+
- **Top level only.** A nested `z.object` still strips unknown keys. To reject
|
|
124
|
+
them, declare that field `z.strictObject(…)`; Zod then reports it as
|
|
125
|
+
`Invalid arguments: … Unrecognized key: "frm"`, without a suggestion.
|
|
126
|
+
- **An input that declares itself open is not checked**: `z.looseObject`,
|
|
127
|
+
`.catchall()` or `z.record` at the top level has no unknown keys.
|
|
128
|
+
- A suggestion is offered when a declared name is within 2 edits (insertion,
|
|
129
|
+
deletion, substitution or swapping two adjacent letters), and those edits
|
|
130
|
+
are at most half its length. At most 20 unknown keys are named per error.
|
|
131
|
+
Echoed keys have control characters and newlines replaced.
|
|
132
|
+
- `registry.unknownArgs(def, args)` returns `[{ key, suggestion? }]`, and is
|
|
133
|
+
optional on the type for the same reason as `evaluate`. Without it, the
|
|
134
|
+
dispatcher still honours a tool's own `strictArgs: true`.
|
|
135
|
+
- The error text is rendered output and is not covered by semver.
|
|
136
|
+
|
|
137
|
+
## Testing: `@george43g/mcp-kit/testing`
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { assertMcpConformance, validateStructured } from "@george43g/mcp-kit/testing";
|
|
141
|
+
|
|
142
|
+
it("every tool is accepted by a 2020-12 client", () => {
|
|
143
|
+
assertMcpConformance(makeAppRegistry(), {
|
|
144
|
+
samples: { noop: { input: [{ input: "x" }], output: [{ echo: "x", engine: "ts", durationMicros: 1 }] } },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("noop's structuredContent matches its outputSchema", async () => {
|
|
149
|
+
const result = await callMcpTool("noop", { input: "x" });
|
|
150
|
+
expect(validateStructured(noopTool, result).errors).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `assertMcpConformance(registry | definitions | Tool[])` compiles every input
|
|
155
|
+
and output schema under **Ajv2020 in strict mode** with no draft-07
|
|
156
|
+
meta-schema loaded — so a draft-07 label fails here exactly as it failed in
|
|
157
|
+
Claude Code. It collects every problem and throws one `McpConformanceError`.
|
|
158
|
+
A registry is checked in full: devOnly and filtered tools too.
|
|
159
|
+
- `validateStructured(tool, result)` is the SDK client's own `tools/call` check:
|
|
160
|
+
structured content required unless `isError`, and validated against
|
|
161
|
+
`outputSchema` when present. It returns `{ valid, errors }`.
|
|
162
|
+
- `ajv` / `ajv-formats` are dependencies of this package, imported only by this
|
|
163
|
+
entry. `@modelcontextprotocol/sdk` already depends on both, so they add nothing
|
|
164
|
+
to an install, and a server that never imports `/testing` never loads them.
|
|
165
|
+
- `allowUnionTypes` is on: `type: ["string", "number"]` is valid 2020-12 (Zod
|
|
166
|
+
emits it for `z.union([z.string(), z.number()])`); Ajv's strict mode flags it
|
|
167
|
+
only as style.
|
|
9
168
|
|
|
10
169
|
## The dispatcher is the point
|
|
11
170
|
|
|
@@ -128,6 +287,60 @@ The marker matters more than it looks: a **silently** shortened document is
|
|
|
128
287
|
indistinguishable from a document that really ended there, and a model reading it
|
|
129
288
|
will answer confidently from the fragment it received.
|
|
130
289
|
|
|
290
|
+
## Migrating from 2.x to 3.0.0
|
|
291
|
+
|
|
292
|
+
**Why:** 2.x emitted draft-07 JSON Schema, and Claude Code 2.1.292+ (a
|
|
293
|
+
2020-12-only client) throws on it, so every tool from every 2.x server failed in
|
|
294
|
+
new sessions. 3.0.0 moves to Zod 4's native converter and 2020-12.
|
|
295
|
+
|
|
296
|
+
Breaking changes, each with its migration:
|
|
297
|
+
|
|
298
|
+
1. **`zod` is a peer, `^4`** (was a dependency, `^3.23`). Install `zod@^4` in
|
|
299
|
+
your app and drop `zod-to-json-schema`. Zod 4's `zod` root keeps `z.object`,
|
|
300
|
+
`.describe()`, `.default()`, `z.enum`, `z.infer`; see Zod's own v4 migration
|
|
301
|
+
guide for `.errors` → `.issues`, `z.string().email()` → `z.email()` (the old
|
|
302
|
+
form still works), and error-map changes.
|
|
303
|
+
2. **Wire schemas are JSON Schema 2020-12 with no `$schema`** (were draft-07,
|
|
304
|
+
labelled). Clients that pinned draft-07 behaviour see 2020-12 keywords:
|
|
305
|
+
`prefixItems` for tuples, `$defs` for recursion. Nothing to do unless you
|
|
306
|
+
post-process `inputSchema` — and if you stripped `$schema` yourself as a
|
|
307
|
+
hot-fix, delete that code.
|
|
308
|
+
3. **Input objects are open; output objects stay closed.** `inputSchema` is
|
|
309
|
+
the input side of the schema (`io: "input"`) and no longer carries
|
|
310
|
+
`additionalProperties: false`, so a client may send extra keys — the
|
|
311
|
+
dispatcher's Zod parse strips them, as before. `.default()` fields stay
|
|
312
|
+
optional on input (unchanged) and required on output. `outputSchema` keeps
|
|
313
|
+
`additionalProperties: false`, as in 2.x.
|
|
314
|
+
4. **`makeRegistry()` throws at construction for an unrepresentable schema**,
|
|
315
|
+
naming the tool: `z.date()`, `z.bigint()`, `z.map()`, `z.set()`,
|
|
316
|
+
`z.symbol()`, `z.undefined()`, an output-side `.transform()`. 2.x emitted a
|
|
317
|
+
best-effort schema silently — `z.date()` became `{type: "string", format:
|
|
318
|
+
"date-time"}`, `z.map()` an array of pairs. **The common case is a `z.date()`
|
|
319
|
+
output field:** return `date.toISOString()` and declare
|
|
320
|
+
`z.string().datetime()` (or `z.iso.datetime()`).
|
|
321
|
+
5. **`makeRegistry()` throws when a tool's input or output is not a
|
|
322
|
+
`z.object`** (MCP requires `type: "object"`). Wrap the value:
|
|
323
|
+
`z.object({ result: … })`.
|
|
324
|
+
6. **`ToolRegistry` has a new required member, `allows(def, ctx?)`**, and
|
|
325
|
+
`toMcpTools()` takes an optional second argument. Only a hand-written
|
|
326
|
+
`ToolRegistry` implementation breaks; build it with `makeRegistry()`.
|
|
327
|
+
7. **"Invalid arguments" text changes wording**: the per-field lines come from
|
|
328
|
+
Zod 4's messages (`Invalid input: expected string, received number`, was
|
|
329
|
+
`Expected string, received number`). Rendered output is not covered by
|
|
330
|
+
semver; listed because a test that snapshots the text will notice.
|
|
331
|
+
8. **The SDK dependency floor is unchanged (`^1.29.0`)** — 1.29 already accepts
|
|
332
|
+
`zod ^3.25 || ^4`. Listed so nobody hunts for it.
|
|
333
|
+
|
|
334
|
+
Additive, no action needed: `toMcpSchema()`, the registry `filter` option and
|
|
335
|
+
the `ctx` argument to `dispatch`, and the `@george43g/mcp-kit/testing` entry.
|
|
336
|
+
|
|
337
|
+
Add one test so this never regresses silently:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { assertMcpConformance } from "@george43g/mcp-kit/testing";
|
|
341
|
+
it("tools conform", () => void assertMcpConformance(makeAppRegistry()));
|
|
342
|
+
```
|
|
343
|
+
|
|
131
344
|
## Upgrading to 1.0.0
|
|
132
345
|
|
|
133
346
|
**One breaking change, and it affects you only if you register `devOnly` tools.**
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* The exported `buildDispatcher` returns a function whose shape matches the
|
|
18
18
|
* SDK's `CallToolRequest` handler.
|
|
19
19
|
*/
|
|
20
|
-
import type
|
|
20
|
+
import { type ContentBlock, type ToolFilterContext, type ToolRegistry } from "./tool-registry.js";
|
|
21
21
|
export interface ToolResult {
|
|
22
22
|
content: ContentBlock[];
|
|
23
23
|
structuredContent?: unknown;
|
|
@@ -73,6 +73,14 @@ export interface BuildDispatcherOptions {
|
|
|
73
73
|
*/
|
|
74
74
|
devOnlyEnabled?: () => boolean;
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Call a tool by name.
|
|
78
|
+
*
|
|
79
|
+
* `ctx` is what the registry's `filter` (see `makeRegistry`) is evaluated
|
|
80
|
+
* against. From an MCP `tools/call` handler, pass the handler's `extra`:
|
|
81
|
+
* `dispatch(name, args, extra.signal, extra)`. An in-process caller omits it,
|
|
82
|
+
* and the filter sees `{}`.
|
|
83
|
+
*/
|
|
84
|
+
export type Dispatch = (name: string, args: unknown, signal?: AbortSignal, ctx?: ToolFilterContext) => Promise<ToolResult>;
|
|
77
85
|
export declare function buildDispatcher(opts: BuildDispatcherOptions): Dispatch;
|
|
78
86
|
//# sourceMappingURL=dispatch.d.ts.map
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAEtB,KAAK,YAAY,EAGlB,MAAM,oBAAoB,CAAC;AAiC5B,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,YAAY,CAAC;IACvB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,CACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,WAAW,EACpB,GAAG,CAAC,EAAE,iBAAiB,KACpB,OAAO,CAAC,UAAU,CAAC,CAAC;AA8CzB,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,CA2JtE"}
|
package/dist/dispatch.js
CHANGED
|
@@ -21,6 +21,8 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { envNum, error as logError, warn as logWarn, noteActivity, perf, ToolTimeoutError, withTimeout, } from "@george43g/robustness";
|
|
22
22
|
import { wrapToolError } from "./prompt-injection.js";
|
|
23
23
|
import { sanitize } from "./sanitize.js";
|
|
24
|
+
import { toMcpSchema, } from "./tool-registry.js";
|
|
25
|
+
import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
|
|
24
26
|
/**
|
|
25
27
|
* Replace the absolute home-directory prefix with `~` in anything headed for a
|
|
26
28
|
* log sink.
|
|
@@ -55,7 +57,40 @@ function defaultTimeoutMs() {
|
|
|
55
57
|
return envNum("MCP_TOOL_TIMEOUT_DEFAULT_MS", 30_000);
|
|
56
58
|
}
|
|
57
59
|
function formatZodError(err) {
|
|
58
|
-
return err.
|
|
60
|
+
return err.issues
|
|
61
|
+
.map((e) => ` - ${e.path.map(String).join(".") || "(root)"}: ${e.message}`)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** At most this many unknown keys are named in one error; the rest are counted. */
|
|
65
|
+
const MAX_UNKNOWN_REPORTED = 20;
|
|
66
|
+
function formatUnknownArgs(unknown) {
|
|
67
|
+
const lines = unknown.slice(0, MAX_UNKNOWN_REPORTED).map(({ key, suggestion }) => {
|
|
68
|
+
// The key is caller-supplied and echoed back: neutralise control characters,
|
|
69
|
+
// including the newlines sanitize() keeps, so a key cannot forge a line.
|
|
70
|
+
const shown = (sanitize(key, 128) ?? "").replace(/[\t\n\r]/g, "\uFFFD");
|
|
71
|
+
return suggestion === undefined
|
|
72
|
+
? `unknown argument ${shown}`
|
|
73
|
+
: `unknown argument ${shown}; did you mean ${suggestion}?`;
|
|
74
|
+
});
|
|
75
|
+
const rest = unknown.length - MAX_UNKNOWN_REPORTED;
|
|
76
|
+
if (rest > 0)
|
|
77
|
+
lines.push(`and ${rest} more unknown argument(s)`);
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
// Declared top-level argument names per definition, derived from the wire
|
|
81
|
+
// input schema on first need. Weak so a definition can still be collected.
|
|
82
|
+
const declaredCache = new WeakMap();
|
|
83
|
+
function declaredFor(def) {
|
|
84
|
+
if (!declaredCache.has(def)) {
|
|
85
|
+
declaredCache.set(def, declaredArgs(toMcpSchema(def.input, "input", def.name)));
|
|
86
|
+
}
|
|
87
|
+
return declaredCache.get(def) ?? null;
|
|
88
|
+
}
|
|
89
|
+
function unknownArgsHint(def) {
|
|
90
|
+
const declared = declaredFor(def) ?? [];
|
|
91
|
+
return declared.length === 0
|
|
92
|
+
? "This tool takes no arguments."
|
|
93
|
+
: `Valid arguments: ${declared.join(", ")}.`;
|
|
59
94
|
}
|
|
60
95
|
export function buildDispatcher(opts) {
|
|
61
96
|
// Make the ambiguous state unrepresentable, at construction, before a single
|
|
@@ -72,7 +107,22 @@ export function buildDispatcher(opts) {
|
|
|
72
107
|
}
|
|
73
108
|
}
|
|
74
109
|
const fallback = defaultTimeoutMs();
|
|
75
|
-
|
|
110
|
+
const { registry } = opts;
|
|
111
|
+
// A registry built by `makeRegistry()` has both methods; a hand-written one
|
|
112
|
+
// from 3.0 may not, and still gets the boolean gate and per-tool strictArgs.
|
|
113
|
+
const evaluate = (def, ctx) => registry.evaluate
|
|
114
|
+
? registry.evaluate(def, ctx)
|
|
115
|
+
: registry.allows(def, ctx)
|
|
116
|
+
? { allowed: true }
|
|
117
|
+
: { allowed: false };
|
|
118
|
+
const unknownArgs = (def, args) => {
|
|
119
|
+
if (registry.unknownArgs)
|
|
120
|
+
return registry.unknownArgs(def, args);
|
|
121
|
+
if (def.strictArgs !== true)
|
|
122
|
+
return [];
|
|
123
|
+
return findUnknownArgs(declaredFor(def), args);
|
|
124
|
+
};
|
|
125
|
+
return async (name, args, signal, ctx = {}) => {
|
|
76
126
|
noteActivity();
|
|
77
127
|
opts.onCall?.(name);
|
|
78
128
|
const def = opts.registry.get(name);
|
|
@@ -84,7 +134,21 @@ export function buildDispatcher(opts) {
|
|
|
84
134
|
// construction, which the readonly type discourages but does not prevent.
|
|
85
135
|
// If that ever happens the tool is gated, not exposed.
|
|
86
136
|
const devGated = def?.devOnly === true && !(opts.devOnlyEnabled?.() ?? false);
|
|
87
|
-
|
|
137
|
+
// The registry's per-request filter gates the CALL path with the same
|
|
138
|
+
// predicate that gates the listing. Hiding a tool from tools/list while it
|
|
139
|
+
// still answers by name is the hole `devOnly` already fell into once.
|
|
140
|
+
// Evaluated only for a tool that exists and is not dev-gated, so a deny
|
|
141
|
+
// reason can never reveal a dev-only tool.
|
|
142
|
+
const decision = def !== undefined && !devGated ? evaluate(def, ctx) : { allowed: false };
|
|
143
|
+
// `{ deny }`: the filter chose to tell the caller why, so it can recover.
|
|
144
|
+
if (!decision.allowed && decision.deny !== undefined) {
|
|
145
|
+
opts.onError?.(name, new Error("tool_denied"));
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: wrapToolError(name, decision.deny) }],
|
|
148
|
+
isError: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!def || !decision.allowed) {
|
|
88
152
|
opts.onError?.(name, new Error("unknown_tool"));
|
|
89
153
|
return {
|
|
90
154
|
content: [
|
|
@@ -96,6 +160,21 @@ export function buildDispatcher(opts) {
|
|
|
96
160
|
isError: true,
|
|
97
161
|
};
|
|
98
162
|
}
|
|
163
|
+
// strictArgs: refuse an undeclared argument name before parsing, because
|
|
164
|
+
// parsing would strip it silently and run the handler without it.
|
|
165
|
+
const unknown = unknownArgs(def, args ?? {});
|
|
166
|
+
if (unknown.length > 0) {
|
|
167
|
+
opts.onError?.(name, new Error("unknown_arguments"));
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: wrapToolError(name, formatUnknownArgs(unknown), unknownArgsHint(def)),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
99
178
|
const parsed = def.input.safeParse(args ?? {});
|
|
100
179
|
if (!parsed.success) {
|
|
101
180
|
opts.onError?.(name, parsed.error);
|
package/dist/dispatch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACL,MAAM,EACN,KAAK,IAAI,QAAQ,EACjB,IAAI,IAAI,OAAO,EACf,YAAY,EACZ,IAAI,EACJ,gBAAgB,EAChB,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { type WrapInstructionsResult, wrapInstructions, wrapToolError, wrapUntru
|
|
|
4
4
|
export type { BuildResourcesHandlerOptions, ResourceContent, ResourceHandlers, ResourceListEntry, ResourcesProvider, ResourceTemplate, } from "./resources.js";
|
|
5
5
|
export { buildResourcesHandler } from "./resources.js";
|
|
6
6
|
export { CONTENT_BUDGET, sanitize, sanitizeContent } from "./sanitize.js";
|
|
7
|
-
export type { AnyToolDefinition, ContentBlock, ToolDefinition, ToolRegistry, } from "./tool-registry.js";
|
|
8
|
-
export { makeRegistry } from "./tool-registry.js";
|
|
7
|
+
export type { AnyToolDefinition, AnyZodSchema, ContentBlock, MakeRegistryOptions, McpJsonSchema, McpSchemaIo, ToolDefinition, ToolDeny, ToolFilter, ToolFilterContext, ToolFilterDecision, ToolFilterResult, ToolRegistry, UnknownArgument, } from "./tool-registry.js";
|
|
8
|
+
export { makeRegistry, toMcpSchema } from "./tool-registry.js";
|
|
9
9
|
export type { HttpServerHandle, HttpServerOptions } from "./transports/http.js";
|
|
10
10
|
export { startHttpServer } from "./transports/http.js";
|
|
11
11
|
export type { StartStdioOptions } from "./transports/stdio.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,4BAA4B,EAC5B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,4BAA4B,EAC5B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,cAAc,EACd,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { buildDispatcher } from "./dispatch.js";
|
|
|
2
2
|
export { wrapInstructions, wrapToolError, wrapUntrusted, } from "./prompt-injection.js";
|
|
3
3
|
export { buildResourcesHandler } from "./resources.js";
|
|
4
4
|
export { CONTENT_BUDGET, sanitize, sanitizeContent } from "./sanitize.js";
|
|
5
|
-
export { makeRegistry } from "./tool-registry.js";
|
|
5
|
+
export { makeRegistry, toMcpSchema } from "./tool-registry.js";
|
|
6
6
|
export { startHttpServer } from "./transports/http.js";
|
|
7
7
|
export { startStdio } from "./transports/stdio.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAEL,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAS/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAEL,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAS/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAiB1E,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@george43g/mcp-kit/testing` — MCP wire-conformance checks for a consumer's
|
|
3
|
+
* test suite.
|
|
4
|
+
*
|
|
5
|
+
* Two questions, each one a client has actually failed on:
|
|
6
|
+
*
|
|
7
|
+
* 1. Does every tool's `inputSchema` / `outputSchema` compile in a client that
|
|
8
|
+
* only speaks JSON Schema 2020-12? (`assertMcpConformance`) Claude Code
|
|
9
|
+
* 2.1.292 refused every mcp-kit 2.x tool because the schemas were labelled
|
|
10
|
+
* draft-07.
|
|
11
|
+
* 2. Does a tool's `structuredContent` satisfy its own `outputSchema`?
|
|
12
|
+
* (`validateStructured`) The SDK client runs exactly this check on every
|
|
13
|
+
* `tools/call` and turns a mismatch into an error.
|
|
14
|
+
*
|
|
15
|
+
* A separate entry so Ajv is never loaded by a running server: nothing under
|
|
16
|
+
* the main entry imports this file. `ajv` and `ajv-formats` are regular
|
|
17
|
+
* dependencies of the package rather than optional peers because
|
|
18
|
+
* `@modelcontextprotocol/sdk` already depends on both (^8.17.1 / ^3.0.1) — they
|
|
19
|
+
* are in every consumer's install already, so declaring them adds no package,
|
|
20
|
+
* and a consumer's vitest needs no extra devDependency to use this entry.
|
|
21
|
+
*/
|
|
22
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { type AnyToolDefinition, type ToolRegistry } from "./tool-registry.js";
|
|
24
|
+
/** Anything `assertMcpConformance` can check: a registry, its definitions, or a wire `tools/list`. */
|
|
25
|
+
export type ConformanceSource = ToolRegistry | readonly AnyToolDefinition[] | readonly Tool[];
|
|
26
|
+
/** Known-good values for one tool. Each must validate against the tool's wire schema. */
|
|
27
|
+
export interface ConformanceSamples {
|
|
28
|
+
input?: readonly unknown[];
|
|
29
|
+
output?: readonly unknown[];
|
|
30
|
+
}
|
|
31
|
+
export interface ConformanceOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Known-good values per tool name. Input samples are validated against
|
|
34
|
+
* `inputSchema`, output samples against `outputSchema` — under Ajv, i.e. as
|
|
35
|
+
* a client sees them. Catches a schema that compiles but rejects real data.
|
|
36
|
+
* A sample for a tool name that is not in the source is itself a failure, so
|
|
37
|
+
* a renamed tool cannot silently stop being checked.
|
|
38
|
+
*/
|
|
39
|
+
samples?: Readonly<Record<string, ConformanceSamples>>;
|
|
40
|
+
}
|
|
41
|
+
export interface ConformanceReport {
|
|
42
|
+
/** Names of the tools checked, in source order. */
|
|
43
|
+
tools: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface StructuredValidation {
|
|
46
|
+
valid: boolean;
|
|
47
|
+
/** One line per problem; empty when `valid`. */
|
|
48
|
+
errors: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Thrown by `assertMcpConformance`; `problems` holds one line per failure. */
|
|
51
|
+
export declare class McpConformanceError extends Error {
|
|
52
|
+
readonly problems: readonly string[];
|
|
53
|
+
constructor(problems: readonly string[]);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Throw `McpConformanceError` unless every tool's schemas would be accepted by a
|
|
57
|
+
* JSON Schema 2020-12 client, and every sample validates.
|
|
58
|
+
*
|
|
59
|
+
* Collects every problem before throwing, so one run shows them all.
|
|
60
|
+
*
|
|
61
|
+
* A `ToolRegistry` is checked in full — devOnly tools included and its
|
|
62
|
+
* `filter` ignored — because a hidden tool still ships a schema somebody's
|
|
63
|
+
* client will compile.
|
|
64
|
+
*
|
|
65
|
+
* Pass the result of a real `tools/list` (`Tool[]`) to check the wire output
|
|
66
|
+
* itself; pass definitions or a registry to check what `toMcpSchema` would emit.
|
|
67
|
+
*/
|
|
68
|
+
export declare function assertMcpConformance(source: ConformanceSource, opts?: ConformanceOptions): ConformanceReport;
|
|
69
|
+
/**
|
|
70
|
+
* Check a `tools/call` result the way the SDK client does before handing it to
|
|
71
|
+
* the model:
|
|
72
|
+
*
|
|
73
|
+
* - a tool with an `outputSchema` must return `structuredContent` unless the
|
|
74
|
+
* result is an error;
|
|
75
|
+
* - `structuredContent`, when present, must validate against `outputSchema`
|
|
76
|
+
* (under Ajv2020, strict).
|
|
77
|
+
*
|
|
78
|
+
* A tool with no `outputSchema` has nothing to check and is always valid.
|
|
79
|
+
* Accepts the wire `Tool` (from `tools/list`) or a `ToolDefinition`.
|
|
80
|
+
*
|
|
81
|
+
* Returns rather than throws, so a test reads
|
|
82
|
+
* `expect(validateStructured(tool, result).errors).toEqual([])` and the diff
|
|
83
|
+
* shows the reason.
|
|
84
|
+
*/
|
|
85
|
+
export declare function validateStructured(tool: Tool | AnyToolDefinition, result: {
|
|
86
|
+
structuredContent?: unknown;
|
|
87
|
+
isError?: boolean | undefined;
|
|
88
|
+
} | Record<string, unknown>): StructuredValidation;
|
|
89
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAG/D,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAU5B,sGAAsG;AACtG,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,SAAS,iBAAiB,EAAE,GAAG,SAAS,IAAI,EAAE,CAAC;AAE9F,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,+EAA+E;AAC/E,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;gBACzB,QAAQ,EAAE,SAAS,MAAM,EAAE;CAOxC;AAuFD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,iBAAiB,EACzB,IAAI,GAAE,kBAAuB,GAC5B,iBAAiB,CA4DnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,GAAG,iBAAiB,EAG9B,MAAM,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/F,oBAAoB,CAmCtB"}
|