@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.
@@ -0,0 +1,230 @@
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 { Ajv2020 } from "ajv/dist/2020.js";
23
+ import addFormatsModule from "ajv-formats";
24
+ import { toMcpSchema, } from "./tool-registry.js";
25
+ // ajv-formats is CommonJS with `module.exports = plugin` and `.default = plugin`;
26
+ // under NodeNext the default import is the module object, so take `.default`
27
+ // when it is there.
28
+ const addFormats = (addFormatsModule.default ??
29
+ addFormatsModule);
30
+ const DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
31
+ /** Thrown by `assertMcpConformance`; `problems` holds one line per failure. */
32
+ export class McpConformanceError extends Error {
33
+ problems;
34
+ constructor(problems) {
35
+ super(`MCP conformance failed (${problems.length} problem(s)):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
36
+ this.name = "McpConformanceError";
37
+ this.problems = problems;
38
+ }
39
+ }
40
+ /**
41
+ * The Ajv a 2020-12-only client effectively runs: strict mode, 2020-12 as the
42
+ * only meta-schema (no draft-07 is added, so a draft-07 `$schema` fails here
43
+ * exactly as it failed in the client), standard formats known.
44
+ *
45
+ * `allowUnionTypes` is the one relaxation, and it is not a relaxation of the
46
+ * spec: `type: ["string", "number"]` is valid 2020-12 — it is what Zod emits for
47
+ * `z.union([z.string(), z.number()])` — and Ajv's `strictTypes` flags it only
48
+ * as a style preference.
49
+ */
50
+ function makeAjv() {
51
+ const ajv = new Ajv2020({ strict: true, allowUnionTypes: true, allErrors: true });
52
+ addFormats(ajv);
53
+ return ajv;
54
+ }
55
+ function formatAjvErrors(errors) {
56
+ return (errors ?? [])
57
+ .map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim())
58
+ .join("; ");
59
+ }
60
+ function isRegistry(source) {
61
+ return !Array.isArray(source) && typeof source.toMcpTools === "function";
62
+ }
63
+ function isDefinition(entry) {
64
+ return "input" in entry && "output" in entry;
65
+ }
66
+ /** Convert definitions to wire shape, recording a conversion throw as a problem. */
67
+ function toWire(entry, problems) {
68
+ if (!isDefinition(entry))
69
+ return entry;
70
+ const wire = { name: entry.name };
71
+ for (const io of ["input", "output"]) {
72
+ try {
73
+ const schema = toMcpSchema(io === "input" ? entry.input : entry.output, io, entry.name);
74
+ if (io === "input")
75
+ wire.inputSchema = schema;
76
+ else
77
+ wire.outputSchema = schema;
78
+ }
79
+ catch (err) {
80
+ problems.push(err.message);
81
+ }
82
+ }
83
+ return wire;
84
+ }
85
+ function compileWireSchema(ajv, tool, field, schema, problems) {
86
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
87
+ problems.push(`${tool}: ${field} is not a JSON Schema object.`);
88
+ return undefined;
89
+ }
90
+ const s = schema;
91
+ if (s.$schema !== undefined && s.$schema !== DIALECT_2020_12) {
92
+ problems.push(`${tool}: ${field} declares $schema ${JSON.stringify(s.$schema)}. A 2020-12-only client ` +
93
+ "(Claude Code 2.1.292+) refuses it. Emit 2020-12 with no $schema — mcp-kit's toMcpSchema() does.");
94
+ }
95
+ if (s.type !== "object") {
96
+ problems.push(`${tool}: ${field} has type ${JSON.stringify(s.type ?? null)}; MCP requires "object".`);
97
+ }
98
+ try {
99
+ return ajv.compile(s);
100
+ }
101
+ catch (err) {
102
+ problems.push(`${tool}: ${field} does not compile under Ajv2020 strict: ${err.message}`);
103
+ return undefined;
104
+ }
105
+ }
106
+ /**
107
+ * Throw `McpConformanceError` unless every tool's schemas would be accepted by a
108
+ * JSON Schema 2020-12 client, and every sample validates.
109
+ *
110
+ * Collects every problem before throwing, so one run shows them all.
111
+ *
112
+ * A `ToolRegistry` is checked in full — devOnly tools included and its
113
+ * `filter` ignored — because a hidden tool still ships a schema somebody's
114
+ * client will compile.
115
+ *
116
+ * Pass the result of a real `tools/list` (`Tool[]`) to check the wire output
117
+ * itself; pass definitions or a registry to check what `toMcpSchema` would emit.
118
+ */
119
+ export function assertMcpConformance(source, opts = {}) {
120
+ const problems = [];
121
+ const entries = isRegistry(source) ? source.tools : source;
122
+ const ajv = makeAjv();
123
+ const seen = new Set();
124
+ const names = [];
125
+ const validators = new Map();
126
+ for (const entry of entries) {
127
+ const wire = toWire(entry, problems);
128
+ const name = typeof wire.name === "string" ? wire.name : "";
129
+ if (name === "") {
130
+ problems.push("A tool has no name.");
131
+ continue;
132
+ }
133
+ if (seen.has(name))
134
+ problems.push(`${name}: duplicate tool name.`);
135
+ seen.add(name);
136
+ names.push(name);
137
+ const v = {};
138
+ if (wire.inputSchema === undefined) {
139
+ if (!isDefinition(entry))
140
+ problems.push(`${name}: no inputSchema (MCP requires one).`);
141
+ }
142
+ else {
143
+ v.input = compileWireSchema(ajv, name, "inputSchema", wire.inputSchema, problems);
144
+ }
145
+ if (wire.outputSchema !== undefined) {
146
+ v.output = compileWireSchema(ajv, name, "outputSchema", wire.outputSchema, problems);
147
+ }
148
+ validators.set(name, v);
149
+ }
150
+ for (const [tool, samples] of Object.entries(opts.samples ?? {})) {
151
+ const v = validators.get(tool);
152
+ if (!v) {
153
+ problems.push(`samples given for "${tool}", which is not in the source.`);
154
+ continue;
155
+ }
156
+ for (const io of ["input", "output"]) {
157
+ const list = samples[io] ?? [];
158
+ if (list.length === 0)
159
+ continue;
160
+ const validate = v[io];
161
+ if (!validate) {
162
+ problems.push(`${tool}: ${io} samples given but there is no compiled ${io}Schema.`);
163
+ continue;
164
+ }
165
+ list.forEach((sample, i) => {
166
+ if (!validate(sample)) {
167
+ problems.push(`${tool}: ${io} sample #${i} rejected: ${formatAjvErrors(validate.errors)}`);
168
+ }
169
+ });
170
+ }
171
+ }
172
+ if (problems.length > 0)
173
+ throw new McpConformanceError(problems);
174
+ return { tools: names };
175
+ }
176
+ /**
177
+ * Check a `tools/call` result the way the SDK client does before handing it to
178
+ * the model:
179
+ *
180
+ * - a tool with an `outputSchema` must return `structuredContent` unless the
181
+ * result is an error;
182
+ * - `structuredContent`, when present, must validate against `outputSchema`
183
+ * (under Ajv2020, strict).
184
+ *
185
+ * A tool with no `outputSchema` has nothing to check and is always valid.
186
+ * Accepts the wire `Tool` (from `tools/list`) or a `ToolDefinition`.
187
+ *
188
+ * Returns rather than throws, so a test reads
189
+ * `expect(validateStructured(tool, result).errors).toEqual([])` and the diff
190
+ * shows the reason.
191
+ */
192
+ export function validateStructured(tool,
193
+ // `Record<string, unknown>` admits the SDK client's `callTool()` return type,
194
+ // whose union includes the legacy `{ toolResult }` shape.
195
+ result) {
196
+ const { structuredContent, isError } = result;
197
+ let outputSchema;
198
+ if (isDefinition(tool)) {
199
+ try {
200
+ outputSchema = toMcpSchema(tool.output, "output", tool.name);
201
+ }
202
+ catch (err) {
203
+ return { valid: false, errors: [err.message] };
204
+ }
205
+ }
206
+ else {
207
+ outputSchema = tool.outputSchema;
208
+ }
209
+ if (outputSchema === undefined)
210
+ return { valid: true, errors: [] };
211
+ if (!structuredContent && !isError) {
212
+ return {
213
+ valid: false,
214
+ errors: [`Tool ${tool.name} has an output schema but did not return structured content.`],
215
+ };
216
+ }
217
+ if (!structuredContent)
218
+ return { valid: true, errors: [] };
219
+ const problems = [];
220
+ const validate = compileWireSchema(makeAjv(), tool.name, "outputSchema", outputSchema, problems);
221
+ if (!validate)
222
+ return { valid: false, errors: problems };
223
+ if (validate(structuredContent))
224
+ return { valid: true, errors: [] };
225
+ return {
226
+ valid: false,
227
+ errors: (validate.errors ?? []).map((e) => `${tool.name}: structuredContent${e.instancePath} ${e.message ?? "is invalid"}`),
228
+ };
229
+ }
230
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,13 @@
7
7
  * - the ToolDefinition (registration metadata)
8
8
  *
9
9
  * The registry collects ToolDefinitions and converts them to the SDK's
10
- * `Tool[]` shape via `toMcpTools()`.
10
+ * `Tool[]` shape via `toMcpTools()`. Both wire schemas are DERIVED from the one
11
+ * Zod definition per tool by `toMcpSchema()`; there is no hand-written JSON
12
+ * Schema path, so the wire contract cannot drift from the validator.
11
13
  */
12
- import type { Tool, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
13
- import type { ZodTypeAny, z } from "zod";
14
+ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
15
+ import type { RequestInfo, Tool, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
16
+ import { z } from "zod";
14
17
  /**
15
18
  * A single MCP content block a tool may emit.
16
19
  *
@@ -33,7 +36,15 @@ export type ContentBlock = {
33
36
  data: string;
34
37
  mimeType: string;
35
38
  };
36
- export interface ToolDefinition<TInput extends ZodTypeAny = ZodTypeAny, TOutput extends ZodTypeAny = ZodTypeAny> {
39
+ /**
40
+ * Any Zod schema, inferring `any` — the default for an unparameterised
41
+ * `ToolDefinition`, so a handler written against it is not forced to narrow
42
+ * `unknown`. This is what `ZodTypeAny` meant under Zod 3; Zod 4's `ZodTypeAny`
43
+ * (a deprecated compat alias) infers `unknown`, which would have made every
44
+ * bare `ToolDefinition` handler a type error on upgrade.
45
+ */
46
+ export type AnyZodSchema = z.ZodType<any, any>;
47
+ export interface ToolDefinition<TInput extends z.ZodType = AnyZodSchema, TOutput extends z.ZodType = AnyZodSchema> {
37
48
  /** Snake_case tool name, e.g. "health_check". */
38
49
  name: string;
39
50
  /** One-paragraph description shown to the LLM. */
@@ -51,6 +62,12 @@ export interface ToolDefinition<TInput extends ZodTypeAny = ZodTypeAny, TOutput
51
62
  timeoutMs?: number;
52
63
  /** When true, only register the tool if `MCP_DEV=1`. Used for `get_logs`. */
53
64
  devOnly?: boolean;
65
+ /**
66
+ * Reject a call that carries an argument name this tool does not declare,
67
+ * before the handler runs. Overrides the registry's `strictArgs` for this
68
+ * tool, in either direction. See `MakeRegistryOptions.strictArgs`.
69
+ */
70
+ strictArgs?: boolean;
54
71
  /** Handler: receives parsed input + AbortSignal, returns structured output. */
55
72
  handler: (input: z.infer<TInput>, signal?: AbortSignal) => Promise<z.infer<TOutput>>;
56
73
  /**
@@ -68,11 +85,183 @@ export interface ToolDefinition<TInput extends ZodTypeAny = ZodTypeAny, TOutput
68
85
  toContent?: (result: z.infer<TOutput>) => ContentBlock[];
69
86
  }
70
87
  export type AnyToolDefinition = ToolDefinition<any, any>;
88
+ /**
89
+ * What a tool filter is told about the request it is answering.
90
+ *
91
+ * A structural subset of the SDK's `RequestHandlerExtra` — the second argument
92
+ * every `server.setRequestHandler` callback receives — so a handler can pass
93
+ * its `extra` straight through: `registry.toMcpTools(dev, extra)` and
94
+ * `dispatch(name, args, extra.signal, extra)`.
95
+ *
96
+ * Every field is optional because most calls carry none of them: stdio has no
97
+ * auth, and an in-process caller (a CLI, a REPL, a test) has no request at all.
98
+ * A filter that gates on scopes therefore sees `authInfo === undefined` there,
99
+ * and should decide deliberately what that means for its tools.
100
+ */
101
+ export interface ToolFilterContext {
102
+ /** Validated access token, when the transport authenticated the request (HTTP + OAuth). */
103
+ authInfo?: AuthInfo;
104
+ /** Transport session id, when the transport has sessions. */
105
+ sessionId?: string;
106
+ /** The originating HTTP request's headers, when there was one. */
107
+ requestInfo?: RequestInfo;
108
+ }
109
+ /**
110
+ * A filter's refusal WITH a reason the caller may see. Returned instead of
111
+ * `false` when the caller can do something about it (re-authenticate, ask for
112
+ * a scope), so the refusal is recoverable instead of a dead end.
113
+ */
114
+ export interface ToolDeny {
115
+ /**
116
+ * Shown to the caller as the error text of a `tools/call` to the tool,
117
+ * wrapped like every other dispatcher error:
118
+ * `Tool "<name>" failed: <deny>`. Must be non-empty; an empty string is
119
+ * treated as a plain `false`.
120
+ */
121
+ deny: string;
122
+ }
123
+ /** What a `ToolFilter` may return. */
124
+ export type ToolFilterResult = boolean | ToolDeny;
125
+ /**
126
+ * The registry's verdict on one tool for one request, from
127
+ * `ToolRegistry.evaluate`. `deny` is present only when the filter returned
128
+ * `{ deny }`; a plain `false`, a throw, or any other value has none, and the
129
+ * dispatcher then answers exactly as for an unknown tool.
130
+ */
131
+ export type ToolFilterDecision = {
132
+ allowed: true;
133
+ } | {
134
+ allowed: false;
135
+ deny?: string;
136
+ };
137
+ /** One argument name a strict tool does not declare, from `ToolRegistry.unknownArgs`. */
138
+ export interface UnknownArgument {
139
+ /** The name as the caller sent it. */
140
+ key: string;
141
+ /** The declared argument it most likely meant (within 2 edits), if any. */
142
+ suggestion?: string;
143
+ }
144
+ /**
145
+ * Per-request tool gate. Return `true` to allow the tool, `false` to hide it
146
+ * from `tools/list` AND refuse calls to it as if it did not exist, or
147
+ * `{ deny: "reason" }` to hide it the same way but refuse a call with that
148
+ * reason instead of the unknown-tool wording.
149
+ *
150
+ * `false` is the right answer when the tool's existence is itself the secret;
151
+ * `{ deny }` when the caller can recover, e.g. gmail's
152
+ * `{ deny: "not available. You may need to re-authenticate with additional scopes" }`.
153
+ *
154
+ * Synchronous on purpose: it runs inside `toMcpTools()`, which is synchronous,
155
+ * and a scope check is a set lookup. Resolve anything slow (a token
156
+ * introspection) before the request reaches the registry — the HTTP transport's
157
+ * auth layer is where that belongs.
158
+ *
159
+ * A filter that THROWS is treated as `false` (fail closed): a bug in the gate
160
+ * must not expose the tool it was written to protect.
161
+ */
162
+ export type ToolFilter<TDef extends AnyToolDefinition = AnyToolDefinition> = (tool: TDef, ctx: ToolFilterContext) => ToolFilterResult;
163
+ /**
164
+ * `TDef` lets a consumer carry its own per-tool metadata into the filter
165
+ * without a cast: declare `interface ScopedTool extends ToolDefinition { scopes:
166
+ * string[] }`, pass `ScopedTool[]`, and the filter receives `ScopedTool`.
167
+ *
168
+ * The filter is called on EVERY `toMcpTools()` / dispatch — never cached — so a
169
+ * predicate that reads live state (scopes granted by a re-auth a minute ago)
170
+ * sees the current value. Read that state inside the predicate.
171
+ */
172
+ export interface MakeRegistryOptions<TDef extends AnyToolDefinition = AnyToolDefinition> {
173
+ /**
174
+ * Optional per-request gate, applied by BOTH `toMcpTools()` and the
175
+ * dispatcher. It lives on the registry rather than on either consumer so the
176
+ * listing and the call path cannot disagree: a filter that only hid a tool
177
+ * from `tools/list` would leave it callable by name (see `devOnly`).
178
+ */
179
+ filter?: ToolFilter<TDef>;
180
+ /**
181
+ * Default for every tool's `strictArgs`; a tool's own `strictArgs` wins.
182
+ * Off unless set.
183
+ *
184
+ * When on, a call that names an argument the tool's advertised `inputSchema`
185
+ * does not list under its top-level `properties` is refused BEFORE parsing
186
+ * and before the handler, naming each unknown key with a "did you mean"
187
+ * suggestion when a declared key is within 2 edits:
188
+ * `unknown argument chatIdentifer; did you mean chatIdentifier?`.
189
+ * When off, Zod strips an unknown key silently and the handler runs as if
190
+ * the argument had not been sent, which is how a misspelled optional
191
+ * argument quietly falls back to its default.
192
+ *
193
+ * - The advertised `inputSchema` is unchanged: no `additionalProperties:
194
+ * false` is added, so clients see the same open schema either way.
195
+ * - Valid input parses exactly as without it: the check only reads the
196
+ * argument names, and parsing still runs the tool's own schema.
197
+ * - Top level only. A nested `z.object` still strips unknown keys; declare
198
+ * it `z.strictObject` to reject them (Zod's own error, no suggestion).
199
+ * - A tool whose input declares itself open at the top level
200
+ * (`z.looseObject`, `.catchall()`, `z.record`) has no unknown keys, and
201
+ * the option does nothing for it.
202
+ */
203
+ strictArgs?: boolean;
204
+ }
71
205
  export interface ToolRegistry {
72
206
  readonly tools: readonly AnyToolDefinition[];
73
207
  get(name: string): AnyToolDefinition | undefined;
74
- /** Convert all tools (or just enabled ones) to MCP SDK shape. */
75
- toMcpTools(includeDevOnly?: boolean): Tool[];
208
+ /**
209
+ * Whether `def` passes the registry's filter for this request. Always `true`
210
+ * when no filter was configured; `false` when the filter returns `false` or
211
+ * `{ deny }`, or throws.
212
+ */
213
+ allows(def: AnyToolDefinition, ctx?: ToolFilterContext): boolean;
214
+ /**
215
+ * `allows`, plus the filter's `{ deny }` reason. Optional so a hand-written
216
+ * registry from 3.0 still type-checks; `makeRegistry()` always provides it,
217
+ * and the dispatcher falls back to `allows` without it.
218
+ */
219
+ evaluate?(def: AnyToolDefinition, ctx?: ToolFilterContext): ToolFilterDecision;
220
+ /**
221
+ * The argument names in `args` that `def` does not declare, when `def` is
222
+ * strict (its own `strictArgs`, else the registry's); `[]` otherwise, and
223
+ * `[]` when `args` is not a plain object. Optional for the same reason as
224
+ * `evaluate`; without it the dispatcher honours a tool's own `strictArgs`.
225
+ */
226
+ unknownArgs?(def: AnyToolDefinition, args: unknown): UnknownArgument[];
227
+ /**
228
+ * Convert tools to MCP SDK shape: devOnly tools only when `includeDevOnly`,
229
+ * and only tools the filter allows for `ctx`. Schemas are converted once, at
230
+ * `makeRegistry()`, so a call here never throws for a schema problem.
231
+ */
232
+ toMcpTools(includeDevOnly?: boolean, ctx?: ToolFilterContext): Tool[];
76
233
  }
77
- export declare function makeRegistry(defs: AnyToolDefinition[]): ToolRegistry;
234
+ /** Which side of a Zod schema to describe: what a caller sends, or what parsing yields. */
235
+ export type McpSchemaIo = "input" | "output";
236
+ /** A JSON Schema object as emitted by `toMcpSchema`: always `type: "object"` at the top. */
237
+ export type McpJsonSchema = {
238
+ type: "object";
239
+ [key: string]: unknown;
240
+ };
241
+ /**
242
+ * Convert a Zod schema to the JSON Schema an MCP client accepts as a tool's
243
+ * `inputSchema` / `outputSchema`.
244
+ *
245
+ * - **Dialect: JSON Schema 2020-12**, the MCP default. Draft-07 output (what
246
+ * `zod-to-json-schema` emitted, and what mcp-kit 2.x shipped) is rejected
247
+ * outright by clients that only load the 2020-12 meta-schema — Claude Code
248
+ * 2.1.292 throws on `"$schema": "http://json-schema.org/draft-07/schema#"`.
249
+ * - **`$schema` is stripped.** 2020-12 is the dialect a schema without a label
250
+ * is read as under MCP, and a schema with no label gives no client a dialect
251
+ * to refuse.
252
+ * - **`io` matters.** `"input"` describes what a caller may send (a field with
253
+ * a `.default()` is optional); `"output"` describes what parsing yields (the
254
+ * same field is required, and objects are closed with
255
+ * `additionalProperties: false`).
256
+ * - **Shared sub-schemas are inlined** (`reused: "inline"`). **Cycles are not
257
+ * inlinable**, so a recursive schema still carries `$defs` / `$ref` (or a
258
+ * root `$ref: "#"`). That is valid 2020-12 and kept as is.
259
+ * - **Unrepresentable types throw**: `z.date()`, `z.bigint()`, a transform on
260
+ * the output side, and so on. The error names the tool, so a registry with a
261
+ * bad schema fails at startup rather than at a client's first `tools/list`.
262
+ * - **The top level must be an object.** MCP requires `type: "object"` for both
263
+ * schemas; anything else throws, naming the tool.
264
+ */
265
+ export declare function toMcpSchema(schema: z.ZodType, io: McpSchemaIo, toolName?: string): McpJsonSchema;
266
+ export declare function makeRegistry<TDef extends AnyToolDefinition = AnyToolDefinition>(defs: TDef[], opts?: MakeRegistryOptions<TDef>): ToolRegistry;
78
267
  //# sourceMappingURL=tool-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGzC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC,oEAAoE;GAClE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,cAAc,CAC7B,MAAM,SAAS,UAAU,GAAG,UAAU,EACtC,OAAO,SAAS,UAAU,GAAG,UAAU;IAEvC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,OAAO,CAAC;IAChB,gFAAgF;IAChF,WAAW,EAAE,eAAe,CAAC;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACrF;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,YAAY,EAAE,CAAC;CAC1D;AAMD,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAEzD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAAC;IACjD,iEAAiE;IACjE,UAAU,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,CAAC;CAC9C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,iBAAiB,EAAE,GAAG,YAAY,CA0BpE"}
1
+ {"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
@@ -7,31 +7,117 @@
7
7
  * - the ToolDefinition (registration metadata)
8
8
  *
9
9
  * The registry collects ToolDefinitions and converts them to the SDK's
10
- * `Tool[]` shape via `toMcpTools()`.
10
+ * `Tool[]` shape via `toMcpTools()`. Both wire schemas are DERIVED from the one
11
+ * Zod definition per tool by `toMcpSchema()`; there is no hand-written JSON
12
+ * Schema path, so the wire contract cannot drift from the validator.
11
13
  */
12
- import { zodToJsonSchema } from "zod-to-json-schema";
13
- export function makeRegistry(defs) {
14
+ import { z } from "zod";
15
+ import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
16
+ /**
17
+ * Convert a Zod schema to the JSON Schema an MCP client accepts as a tool's
18
+ * `inputSchema` / `outputSchema`.
19
+ *
20
+ * - **Dialect: JSON Schema 2020-12**, the MCP default. Draft-07 output (what
21
+ * `zod-to-json-schema` emitted, and what mcp-kit 2.x shipped) is rejected
22
+ * outright by clients that only load the 2020-12 meta-schema — Claude Code
23
+ * 2.1.292 throws on `"$schema": "http://json-schema.org/draft-07/schema#"`.
24
+ * - **`$schema` is stripped.** 2020-12 is the dialect a schema without a label
25
+ * is read as under MCP, and a schema with no label gives no client a dialect
26
+ * to refuse.
27
+ * - **`io` matters.** `"input"` describes what a caller may send (a field with
28
+ * a `.default()` is optional); `"output"` describes what parsing yields (the
29
+ * same field is required, and objects are closed with
30
+ * `additionalProperties: false`).
31
+ * - **Shared sub-schemas are inlined** (`reused: "inline"`). **Cycles are not
32
+ * inlinable**, so a recursive schema still carries `$defs` / `$ref` (or a
33
+ * root `$ref: "#"`). That is valid 2020-12 and kept as is.
34
+ * - **Unrepresentable types throw**: `z.date()`, `z.bigint()`, a transform on
35
+ * the output side, and so on. The error names the tool, so a registry with a
36
+ * bad schema fails at startup rather than at a client's first `tools/list`.
37
+ * - **The top level must be an object.** MCP requires `type: "object"` for both
38
+ * schemas; anything else throws, naming the tool.
39
+ */
40
+ export function toMcpSchema(schema, io, toolName) {
41
+ const where = toolName === undefined ? `${io} schema` : `tool "${toolName}" ${io} schema`;
42
+ let json;
43
+ try {
44
+ json = z.toJSONSchema(schema, { target: "draft-2020-12", io, reused: "inline" });
45
+ }
46
+ catch (err) {
47
+ throw new Error(`toMcpSchema: ${where} cannot be represented in JSON Schema: ${err?.message ?? String(err)}. ` +
48
+ "Use a JSON-representable type (an ISO string instead of z.date(), a number or string instead of z.bigint()), " +
49
+ "or move the transform out of the schema.", { cause: err });
50
+ }
51
+ delete json.$schema;
52
+ if (json.type !== "object") {
53
+ throw new Error(`toMcpSchema: ${where} must be a Zod object (JSON Schema type "object"), got ${JSON.stringify(json.type ?? null)}. ` +
54
+ "MCP requires tool input and output schemas to describe an object; wrap the value, e.g. z.object({ result: ... }).");
55
+ }
56
+ return json;
57
+ }
58
+ /** Read a filter's return value. Anything but `true` or a non-empty `{ deny }` is a plain refusal. */
59
+ function decide(result) {
60
+ if (result === true)
61
+ return { allowed: true };
62
+ if (result !== null && typeof result === "object") {
63
+ const deny = result.deny;
64
+ if (typeof deny === "string" && deny.trim() !== "")
65
+ return { allowed: false, deny };
66
+ }
67
+ return { allowed: false };
68
+ }
69
+ export function makeRegistry(defs, opts = {}) {
14
70
  const byName = new Map(defs.map((d) => [d.name, d]));
71
+ // Converted eagerly: a schema that cannot be represented throws HERE, at
72
+ // startup, naming the tool, instead of on a client's first tools/list.
73
+ // Keyed by definition, not name, so two definitions sharing a name (a
74
+ // mistake, but representable) cannot swap schemas.
75
+ const wire = new Map(defs.map((d) => [
76
+ d,
77
+ {
78
+ name: d.name,
79
+ description: d.description,
80
+ annotations: d.annotations,
81
+ inputSchema: toMcpSchema(d.input, "input", d.name),
82
+ outputSchema: toMcpSchema(d.output, "output", d.name),
83
+ },
84
+ ]));
85
+ const { filter, strictArgs = false } = opts;
86
+ // Derived from the wire schema computed above, once per definition.
87
+ const declared = new Map(defs.map((d) => [d, declaredArgs(wire.get(d).inputSchema)]));
88
+ const evaluate = (def, ctx = {}) => {
89
+ if (!filter)
90
+ return { allowed: true };
91
+ try {
92
+ // Only definitions from `defs` reach here in practice; the dispatcher
93
+ // looks them up through `get()`.
94
+ return decide(filter(def, ctx));
95
+ }
96
+ catch {
97
+ return { allowed: false };
98
+ }
99
+ };
100
+ const allows = (def, ctx) => evaluate(def, ctx).allowed;
15
101
  return {
16
102
  tools: defs,
17
103
  get(name) {
18
104
  return byName.get(name);
19
105
  },
20
- toMcpTools(includeDevOnly = false) {
106
+ allows,
107
+ evaluate,
108
+ unknownArgs(def, args) {
109
+ if (!(def.strictArgs ?? strictArgs))
110
+ return [];
111
+ // A definition from outside `defs` has no precomputed entry; derive it.
112
+ const keys = declared.has(def)
113
+ ? (declared.get(def) ?? null)
114
+ : declaredArgs(toMcpSchema(def.input, "input", def.name));
115
+ return findUnknownArgs(keys, args);
116
+ },
117
+ toMcpTools(includeDevOnly = false, ctx = {}) {
21
118
  return defs
22
- .filter((d) => includeDevOnly || !d.devOnly)
23
- .map((d) =>
24
- // The SDK Tool type narrows inputSchema/outputSchema to a JSON-
25
- // schema with `type: "object"` literal; zodToJsonSchema returns
26
- // the broader JsonSchema7Type union. Cast is safe because we
27
- // only ever build Zod object schemas as tool I/O.
28
- ({
29
- name: d.name,
30
- description: d.description,
31
- annotations: d.annotations,
32
- inputSchema: zodToJsonSchema(d.input),
33
- outputSchema: zodToJsonSchema(d.output),
34
- }));
119
+ .filter((d) => (includeDevOnly || !d.devOnly) && allows(d, ctx))
120
+ .map((d) => wire.get(d));
35
121
  },
36
122
  };
37
123
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAsErD,MAAM,UAAU,YAAY,CAAC,IAAyB;IACpD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO;QACL,KAAK,EAAE,IAAI;QACX,GAAG,CAAC,IAAI;YACN,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,UAAU,CAAC,cAAc,GAAG,KAAK;YAC/B,OAAO,IAAI;iBACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;iBAC3C,GAAG,CACF,CAAC,CAAC,EAAE,EAAE;YACJ,gEAAgE;YAChE,gEAAgE;YAChE,6DAA6D;YAC7D,kDAAkD;YAClD,CAAC;gBACC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC;gBACrC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC;aACxC,CAAoB,CACxB,CAAC;QACN,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}