@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/dist/testing.js
ADDED
|
@@ -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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"}
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -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 {
|
|
13
|
-
import type {
|
|
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
|
-
|
|
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
|
-
/**
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/tool-registry.js
CHANGED
|
@@ -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 {
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
|