@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 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
- Not yet on npm. It is vendored into generated repos today; see `DEFERRED.md` #25
8
- in the template repo for the publish sequence.
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.**
@@ -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 { ContentBlock, ToolRegistry } from "./tool-registry.js";
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
- export type Dispatch = (name: string, args: unknown, signal?: AbortSignal) => Promise<ToolResult>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAgCrE,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,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAUlG,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,CA6GtE"}
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.errors.map((e) => ` - ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
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
- return async (name, args, signal) => {
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
- if (!def || devGated) {
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);
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
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";
@@ -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,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,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"}
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;AAO1E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,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"}