@george43g/mcp-kit 1.0.0 → 3.0.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,95 @@ 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
+ ## Testing: `@george43g/mcp-kit/testing`
66
+
67
+ ```ts
68
+ import { assertMcpConformance, validateStructured } from "@george43g/mcp-kit/testing";
69
+
70
+ it("every tool is accepted by a 2020-12 client", () => {
71
+ assertMcpConformance(makeAppRegistry(), {
72
+ samples: { noop: { input: [{ input: "x" }], output: [{ echo: "x", engine: "ts", durationMicros: 1 }] } },
73
+ });
74
+ });
75
+
76
+ it("noop's structuredContent matches its outputSchema", async () => {
77
+ const result = await callMcpTool("noop", { input: "x" });
78
+ expect(validateStructured(noopTool, result).errors).toEqual([]);
79
+ });
80
+ ```
81
+
82
+ - `assertMcpConformance(registry | definitions | Tool[])` compiles every input
83
+ and output schema under **Ajv2020 in strict mode** with no draft-07
84
+ meta-schema loaded — so a draft-07 label fails here exactly as it failed in
85
+ Claude Code. It collects every problem and throws one `McpConformanceError`.
86
+ A registry is checked in full: devOnly and filtered tools too.
87
+ - `validateStructured(tool, result)` is the SDK client's own `tools/call` check:
88
+ structured content required unless `isError`, and validated against
89
+ `outputSchema` when present. It returns `{ valid, errors }`.
90
+ - `ajv` / `ajv-formats` are dependencies of this package, imported only by this
91
+ entry. `@modelcontextprotocol/sdk` already depends on both, so they add nothing
92
+ to an install, and a server that never imports `/testing` never loads them.
93
+ - `allowUnionTypes` is on: `type: ["string", "number"]` is valid 2020-12 (Zod
94
+ emits it for `z.union([z.string(), z.number()])`); Ajv's strict mode flags it
95
+ only as style.
9
96
 
10
97
  ## The dispatcher is the point
11
98
 
@@ -128,6 +215,60 @@ The marker matters more than it looks: a **silently** shortened document is
128
215
  indistinguishable from a document that really ended there, and a model reading it
129
216
  will answer confidently from the fragment it received.
130
217
 
218
+ ## Migrating from 2.x to 3.0.0
219
+
220
+ **Why:** 2.x emitted draft-07 JSON Schema, and Claude Code 2.1.292+ (a
221
+ 2020-12-only client) throws on it, so every tool from every 2.x server failed in
222
+ new sessions. 3.0.0 moves to Zod 4's native converter and 2020-12.
223
+
224
+ Breaking changes, each with its migration:
225
+
226
+ 1. **`zod` is a peer, `^4`** (was a dependency, `^3.23`). Install `zod@^4` in
227
+ your app and drop `zod-to-json-schema`. Zod 4's `zod` root keeps `z.object`,
228
+ `.describe()`, `.default()`, `z.enum`, `z.infer`; see Zod's own v4 migration
229
+ guide for `.errors` → `.issues`, `z.string().email()` → `z.email()` (the old
230
+ form still works), and error-map changes.
231
+ 2. **Wire schemas are JSON Schema 2020-12 with no `$schema`** (were draft-07,
232
+ labelled). Clients that pinned draft-07 behaviour see 2020-12 keywords:
233
+ `prefixItems` for tuples, `$defs` for recursion. Nothing to do unless you
234
+ post-process `inputSchema` — and if you stripped `$schema` yourself as a
235
+ hot-fix, delete that code.
236
+ 3. **Input objects are open; output objects stay closed.** `inputSchema` is
237
+ the input side of the schema (`io: "input"`) and no longer carries
238
+ `additionalProperties: false`, so a client may send extra keys — the
239
+ dispatcher's Zod parse strips them, as before. `.default()` fields stay
240
+ optional on input (unchanged) and required on output. `outputSchema` keeps
241
+ `additionalProperties: false`, as in 2.x.
242
+ 4. **`makeRegistry()` throws at construction for an unrepresentable schema**,
243
+ naming the tool: `z.date()`, `z.bigint()`, `z.map()`, `z.set()`,
244
+ `z.symbol()`, `z.undefined()`, an output-side `.transform()`. 2.x emitted a
245
+ best-effort schema silently — `z.date()` became `{type: "string", format:
246
+ "date-time"}`, `z.map()` an array of pairs. **The common case is a `z.date()`
247
+ output field:** return `date.toISOString()` and declare
248
+ `z.string().datetime()` (or `z.iso.datetime()`).
249
+ 5. **`makeRegistry()` throws when a tool's input or output is not a
250
+ `z.object`** (MCP requires `type: "object"`). Wrap the value:
251
+ `z.object({ result: … })`.
252
+ 6. **`ToolRegistry` has a new required member, `allows(def, ctx?)`**, and
253
+ `toMcpTools()` takes an optional second argument. Only a hand-written
254
+ `ToolRegistry` implementation breaks; build it with `makeRegistry()`.
255
+ 7. **"Invalid arguments" text changes wording**: the per-field lines come from
256
+ Zod 4's messages (`Invalid input: expected string, received number`, was
257
+ `Expected string, received number`). Rendered output is not covered by
258
+ semver; listed because a test that snapshots the text will notice.
259
+ 8. **The SDK dependency floor is unchanged (`^1.29.0`)** — 1.29 already accepts
260
+ `zod ^3.25 || ^4`. Listed so nobody hunts for it.
261
+
262
+ Additive, no action needed: `toMcpSchema()`, the registry `filter` option and
263
+ the `ctx` argument to `dispatch`, and the `@george43g/mcp-kit/testing` entry.
264
+
265
+ Add one test so this never regresses silently:
266
+
267
+ ```ts
268
+ import { assertMcpConformance } from "@george43g/mcp-kit/testing";
269
+ it("tools conform", () => void assertMcpConformance(makeAppRegistry()));
270
+ ```
271
+
131
272
  ## Upgrading to 1.0.0
132
273
 
133
274
  **One breaking change, and it affects you only if you register `devOnly` tools.**
@@ -145,6 +286,52 @@ buildDispatcher: 1 devOnly tool(s) registered with no devOnlyEnabled predicate:
145
286
  **Migration is one line** — pass the predicate, as shown in *Dev-only tools*
146
287
  above. If you have no `devOnly` tools, there is nothing to do.
147
288
 
289
+ ### Upgrading to 2.0.0 — declare `@george43g/robustness` yourself
290
+
291
+ **`robustness` moved from `dependencies` to `peerDependencies`.** If your app
292
+ already depends on `@george43g/robustness` directly — as every known consumer
293
+ does, and as the generated template does — **there is nothing to do**. If it does
294
+ not, add it.
295
+
296
+ ```jsonc
297
+ // your package.json
298
+ "dependencies": {
299
+ "@george43g/robustness": ">=0.13.0 <1" // or a caret; any 0.x satisfies the peer
300
+ }
301
+ ```
302
+
303
+ **What this fixes, and it is not cosmetic.** As a plain dependency with a
304
+ *floor*, mcp-kit could resolve its **own second copy** of robustness whenever the
305
+ app's range and mcp-kit's stopped overlapping. The logger keeps prefix and file
306
+ state at module scope, so two copies means **two independent loggers**:
307
+ `setLogFilePrefix` in your app cannot reach the instance mcp-kit's `perf()` spans
308
+ write through, and dispatch spans silently land in the shared `$TMPDIR/mcp/`
309
+ bucket no matter how correctly your app brands itself.
310
+
311
+ That was live in **1.0.0**, whose floor was `>=0.12.0 <1`: an app on `^0.11.0`
312
+ that bumped mcp-kit alone got exactly this split. **2.0.0 removes the failure
313
+ mode by construction** — a peer is supplied by the consumer, so there is only
314
+ ever one instance. The peer range is deliberately wide (`>=0.11.0 <1`, every
315
+ symbol mcp-kit imports exists at 0.11.0), so no consumer is forced to move.
316
+
317
+ `tui-kit` has always declared robustness this way; mcp-kit was the outlier.
318
+
319
+ Verify with the one line that settles it:
320
+
321
+ ```sh
322
+ grep -oE "@george43g/robustness@[0-9.]+" pnpm-lock.yaml | sort -u
323
+ ```
324
+
325
+ One line is correct. Two means something still resolves its own copy.
326
+
327
+ **The generalisable lesson:** a plain dependency with a *wide* range behaves like
328
+ a peer — right up until the ranges stop overlapping. Two individually safe
329
+ changes (raising a floor, cutting a major) were jointly hazardous. Found by
330
+ `up-bank-mcp`.
331
+ Found by `up-bank-mcp` as range arithmetic over the published manifests.
332
+ **Not yet observed in a resolved tree by anyone** — treat the ordering as cheap
333
+ insurance rather than a reproduction.
334
+
148
335
  **Why a throw and not a flipped default.** Before 1.0.0, omitting the predicate
149
336
  left dev-only tools *callable* — hidden from `tools/list` and answering by name.
150
337
  Two consumers hit that independently, and one measured a dev-only log reader
@@ -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, ToolFilterContext, 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,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAgCxF,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;AAYzB,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,CAiHtE"}
package/dist/dispatch.js CHANGED
@@ -55,7 +55,9 @@ function defaultTimeoutMs() {
55
55
  return envNum("MCP_TOOL_TIMEOUT_DEFAULT_MS", 30_000);
56
56
  }
57
57
  function formatZodError(err) {
58
- return err.errors.map((e) => ` - ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
58
+ return err.issues
59
+ .map((e) => ` - ${e.path.map(String).join(".") || "(root)"}: ${e.message}`)
60
+ .join("\n");
59
61
  }
60
62
  export function buildDispatcher(opts) {
61
63
  // Make the ambiguous state unrepresentable, at construction, before a single
@@ -72,7 +74,7 @@ export function buildDispatcher(opts) {
72
74
  }
73
75
  }
74
76
  const fallback = defaultTimeoutMs();
75
- return async (name, args, signal) => {
77
+ return async (name, args, signal, ctx = {}) => {
76
78
  noteActivity();
77
79
  opts.onCall?.(name);
78
80
  const def = opts.registry.get(name);
@@ -84,7 +86,11 @@ export function buildDispatcher(opts) {
84
86
  // construction, which the readonly type discourages but does not prevent.
85
87
  // If that ever happens the tool is gated, not exposed.
86
88
  const devGated = def?.devOnly === true && !(opts.devOnlyEnabled?.() ?? false);
87
- if (!def || devGated) {
89
+ // The registry's per-request filter gates the CALL path with the same
90
+ // predicate that gates the listing. Hiding a tool from tools/list while it
91
+ // still answers by name is the hole `devOnly` already fell into once.
92
+ const filtered = def !== undefined && !opts.registry.allows(def, ctx);
93
+ if (!def || devGated || filtered) {
88
94
  opts.onError?.(name, new Error("unknown_tool"));
89
95
  return {
90
96
  content: [
@@ -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, ToolFilter, ToolFilterContext, ToolRegistry, } 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,UAAU,EACV,iBAAiB,EACjB,YAAY,GACb,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;AAa1E,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"}
@@ -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