@george43g/mcp-kit 2.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.
@@ -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. */
@@ -68,11 +79,104 @@ export interface ToolDefinition<TInput extends ZodTypeAny = ZodTypeAny, TOutput
68
79
  toContent?: (result: z.infer<TOutput>) => ContentBlock[];
69
80
  }
70
81
  export type AnyToolDefinition = ToolDefinition<any, any>;
82
+ /**
83
+ * What a tool filter is told about the request it is answering.
84
+ *
85
+ * A structural subset of the SDK's `RequestHandlerExtra` — the second argument
86
+ * every `server.setRequestHandler` callback receives — so a handler can pass
87
+ * its `extra` straight through: `registry.toMcpTools(dev, extra)` and
88
+ * `dispatch(name, args, extra.signal, extra)`.
89
+ *
90
+ * Every field is optional because most calls carry none of them: stdio has no
91
+ * auth, and an in-process caller (a CLI, a REPL, a test) has no request at all.
92
+ * A filter that gates on scopes therefore sees `authInfo === undefined` there,
93
+ * and should decide deliberately what that means for its tools.
94
+ */
95
+ export interface ToolFilterContext {
96
+ /** Validated access token, when the transport authenticated the request (HTTP + OAuth). */
97
+ authInfo?: AuthInfo;
98
+ /** Transport session id, when the transport has sessions. */
99
+ sessionId?: string;
100
+ /** The originating HTTP request's headers, when there was one. */
101
+ requestInfo?: RequestInfo;
102
+ }
103
+ /**
104
+ * Per-request tool gate: return `false` to hide a tool from `tools/list` AND
105
+ * refuse calls to it.
106
+ *
107
+ * Synchronous on purpose: it runs inside `toMcpTools()`, which is synchronous,
108
+ * and a scope check is a set lookup. Resolve anything slow (a token
109
+ * introspection) before the request reaches the registry — the HTTP transport's
110
+ * auth layer is where that belongs.
111
+ *
112
+ * A filter that THROWS is treated as `false` (fail closed): a bug in the gate
113
+ * must not expose the tool it was written to protect.
114
+ */
115
+ export type ToolFilter<TDef extends AnyToolDefinition = AnyToolDefinition> = (tool: TDef, ctx: ToolFilterContext) => boolean;
116
+ /**
117
+ * `TDef` lets a consumer carry its own per-tool metadata into the filter
118
+ * without a cast: declare `interface ScopedTool extends ToolDefinition { scopes:
119
+ * string[] }`, pass `ScopedTool[]`, and the filter receives `ScopedTool`.
120
+ *
121
+ * The filter is called on EVERY `toMcpTools()` / dispatch — never cached — so a
122
+ * predicate that reads live state (scopes granted by a re-auth a minute ago)
123
+ * sees the current value. Read that state inside the predicate.
124
+ */
125
+ export interface MakeRegistryOptions<TDef extends AnyToolDefinition = AnyToolDefinition> {
126
+ /**
127
+ * Optional per-request gate, applied by BOTH `toMcpTools()` and the
128
+ * dispatcher. It lives on the registry rather than on either consumer so the
129
+ * listing and the call path cannot disagree: a filter that only hid a tool
130
+ * from `tools/list` would leave it callable by name (see `devOnly`).
131
+ */
132
+ filter?: ToolFilter<TDef>;
133
+ }
71
134
  export interface ToolRegistry {
72
135
  readonly tools: readonly AnyToolDefinition[];
73
136
  get(name: string): AnyToolDefinition | undefined;
74
- /** Convert all tools (or just enabled ones) to MCP SDK shape. */
75
- toMcpTools(includeDevOnly?: boolean): Tool[];
137
+ /**
138
+ * Whether `def` passes the registry's filter for this request. Always `true`
139
+ * when no filter was configured; `false` when the filter throws.
140
+ */
141
+ allows(def: AnyToolDefinition, ctx?: ToolFilterContext): boolean;
142
+ /**
143
+ * Convert tools to MCP SDK shape: devOnly tools only when `includeDevOnly`,
144
+ * and only tools the filter allows for `ctx`. Schemas are converted once, at
145
+ * `makeRegistry()`, so a call here never throws for a schema problem.
146
+ */
147
+ toMcpTools(includeDevOnly?: boolean, ctx?: ToolFilterContext): Tool[];
76
148
  }
77
- export declare function makeRegistry(defs: AnyToolDefinition[]): ToolRegistry;
149
+ /** Which side of a Zod schema to describe: what a caller sends, or what parsing yields. */
150
+ export type McpSchemaIo = "input" | "output";
151
+ /** A JSON Schema object as emitted by `toMcpSchema`: always `type: "object"` at the top. */
152
+ export type McpJsonSchema = {
153
+ type: "object";
154
+ [key: string]: unknown;
155
+ };
156
+ /**
157
+ * Convert a Zod schema to the JSON Schema an MCP client accepts as a tool's
158
+ * `inputSchema` / `outputSchema`.
159
+ *
160
+ * - **Dialect: JSON Schema 2020-12**, the MCP default. Draft-07 output (what
161
+ * `zod-to-json-schema` emitted, and what mcp-kit 2.x shipped) is rejected
162
+ * outright by clients that only load the 2020-12 meta-schema — Claude Code
163
+ * 2.1.292 throws on `"$schema": "http://json-schema.org/draft-07/schema#"`.
164
+ * - **`$schema` is stripped.** 2020-12 is the dialect a schema without a label
165
+ * is read as under MCP, and a schema with no label gives no client a dialect
166
+ * to refuse.
167
+ * - **`io` matters.** `"input"` describes what a caller may send (a field with
168
+ * a `.default()` is optional); `"output"` describes what parsing yields (the
169
+ * same field is required, and objects are closed with
170
+ * `additionalProperties: false`).
171
+ * - **Shared sub-schemas are inlined** (`reused: "inline"`). **Cycles are not
172
+ * inlinable**, so a recursive schema still carries `$defs` / `$ref` (or a
173
+ * root `$ref: "#"`). That is valid 2020-12 and kept as is.
174
+ * - **Unrepresentable types throw**: `z.date()`, `z.bigint()`, a transform on
175
+ * the output side, and so on. The error names the tool, so a registry with a
176
+ * bad schema fails at startup rather than at a client's first `tools/list`.
177
+ * - **The top level must be an object.** MCP requires `type: "object"` for both
178
+ * schemas; anything else throws, naming the tool.
179
+ */
180
+ export declare function toMcpSchema(schema: z.ZodType, io: McpSchemaIo, toolName?: string): McpJsonSchema;
181
+ export declare function makeRegistry<TDef extends AnyToolDefinition = AnyToolDefinition>(defs: TDef[], opts?: MakeRegistryOptions<TDef>): ToolRegistry;
78
182
  //# 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,92 @@
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
+ /**
16
+ * Convert a Zod schema to the JSON Schema an MCP client accepts as a tool's
17
+ * `inputSchema` / `outputSchema`.
18
+ *
19
+ * - **Dialect: JSON Schema 2020-12**, the MCP default. Draft-07 output (what
20
+ * `zod-to-json-schema` emitted, and what mcp-kit 2.x shipped) is rejected
21
+ * outright by clients that only load the 2020-12 meta-schema — Claude Code
22
+ * 2.1.292 throws on `"$schema": "http://json-schema.org/draft-07/schema#"`.
23
+ * - **`$schema` is stripped.** 2020-12 is the dialect a schema without a label
24
+ * is read as under MCP, and a schema with no label gives no client a dialect
25
+ * to refuse.
26
+ * - **`io` matters.** `"input"` describes what a caller may send (a field with
27
+ * a `.default()` is optional); `"output"` describes what parsing yields (the
28
+ * same field is required, and objects are closed with
29
+ * `additionalProperties: false`).
30
+ * - **Shared sub-schemas are inlined** (`reused: "inline"`). **Cycles are not
31
+ * inlinable**, so a recursive schema still carries `$defs` / `$ref` (or a
32
+ * root `$ref: "#"`). That is valid 2020-12 and kept as is.
33
+ * - **Unrepresentable types throw**: `z.date()`, `z.bigint()`, a transform on
34
+ * the output side, and so on. The error names the tool, so a registry with a
35
+ * bad schema fails at startup rather than at a client's first `tools/list`.
36
+ * - **The top level must be an object.** MCP requires `type: "object"` for both
37
+ * schemas; anything else throws, naming the tool.
38
+ */
39
+ export function toMcpSchema(schema, io, toolName) {
40
+ const where = toolName === undefined ? `${io} schema` : `tool "${toolName}" ${io} schema`;
41
+ let json;
42
+ try {
43
+ json = z.toJSONSchema(schema, { target: "draft-2020-12", io, reused: "inline" });
44
+ }
45
+ catch (err) {
46
+ throw new Error(`toMcpSchema: ${where} cannot be represented in JSON Schema: ${err?.message ?? String(err)}. ` +
47
+ "Use a JSON-representable type (an ISO string instead of z.date(), a number or string instead of z.bigint()), " +
48
+ "or move the transform out of the schema.", { cause: err });
49
+ }
50
+ delete json.$schema;
51
+ if (json.type !== "object") {
52
+ throw new Error(`toMcpSchema: ${where} must be a Zod object (JSON Schema type "object"), got ${JSON.stringify(json.type ?? null)}. ` +
53
+ "MCP requires tool input and output schemas to describe an object; wrap the value, e.g. z.object({ result: ... }).");
54
+ }
55
+ return json;
56
+ }
57
+ export function makeRegistry(defs, opts = {}) {
14
58
  const byName = new Map(defs.map((d) => [d.name, d]));
59
+ // Converted eagerly: a schema that cannot be represented throws HERE, at
60
+ // startup, naming the tool, instead of on a client's first tools/list.
61
+ // Keyed by definition, not name, so two definitions sharing a name (a
62
+ // mistake, but representable) cannot swap schemas.
63
+ const wire = new Map(defs.map((d) => [
64
+ d,
65
+ {
66
+ name: d.name,
67
+ description: d.description,
68
+ annotations: d.annotations,
69
+ inputSchema: toMcpSchema(d.input, "input", d.name),
70
+ outputSchema: toMcpSchema(d.output, "output", d.name),
71
+ },
72
+ ]));
73
+ const { filter } = opts;
74
+ const allows = (def, ctx = {}) => {
75
+ if (!filter)
76
+ return true;
77
+ try {
78
+ // Only definitions from `defs` reach here in practice; the dispatcher
79
+ // looks them up through `get()`.
80
+ return filter(def, ctx) === true;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ };
15
86
  return {
16
87
  tools: defs,
17
88
  get(name) {
18
89
  return byName.get(name);
19
90
  },
20
- toMcpTools(includeDevOnly = false) {
91
+ allows,
92
+ toMcpTools(includeDevOnly = false, ctx = {}) {
21
93
  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
- }));
94
+ .filter((d) => (includeDevOnly || !d.devOnly) && allows(d, ctx))
95
+ .map((d) => wire.get(d));
35
96
  },
36
97
  };
37
98
  }
@@ -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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@george43g/mcp-kit",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "MCP server building blocks: tool registry types, dispatcher with timeout/perf/abort/error-wrap baked in, stdio + Streamable HTTP transports, sanitize() for user content, UUID-gated prompt-injection helpers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,10 +33,16 @@
33
33
  "import": "./dist/transports/stdio.js",
34
34
  "default": "./dist/transports/stdio.js"
35
35
  },
36
+ "./testing": {
37
+ "types": "./dist/testing.d.ts",
38
+ "import": "./dist/testing.js",
39
+ "default": "./dist/testing.js"
40
+ },
36
41
  "./package.json": "./package.json"
37
42
  },
38
43
  "files": [
39
44
  "dist",
45
+ "!dist/tsconfig.tsbuildinfo",
40
46
  "src",
41
47
  "!src/**/*.test.ts",
42
48
  "README.md",
@@ -52,8 +58,8 @@
52
58
  },
53
59
  "dependencies": {
54
60
  "@modelcontextprotocol/sdk": "^1.29.0",
55
- "zod": "^3.23.0",
56
- "zod-to-json-schema": "^3.25.0"
61
+ "ajv": "^8.17.1",
62
+ "ajv-formats": "^3.0.1"
57
63
  },
58
64
  "devDependencies": {
59
65
  "@george43g/robustness": "workspace:*",
@@ -67,9 +73,11 @@
67
73
  "semantic-release": "^24.2.0",
68
74
  "semantic-release-monorepo": "^8.0.2",
69
75
  "typescript": "^5.7.0",
70
- "vitest": "^3.2.7"
76
+ "vitest": "^3.2.7",
77
+ "zod": "^4.0.0"
71
78
  },
72
79
  "peerDependencies": {
73
- "@george43g/robustness": ">=0.11.0 <1"
80
+ "@george43g/robustness": ">=0.11.0 <1",
81
+ "zod": "^4.0.0"
74
82
  }
75
83
  }
package/src/dispatch.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import type { ZodError } from "zod";
32
32
  import { wrapToolError } from "./prompt-injection.js";
33
33
  import { sanitize } from "./sanitize.js";
34
- import type { ContentBlock, ToolRegistry } from "./tool-registry.js";
34
+ import type { ContentBlock, ToolFilterContext, ToolRegistry } from "./tool-registry.js";
35
35
 
36
36
  /**
37
37
  * Replace the absolute home-directory prefix with `~` in anything headed for a
@@ -121,14 +121,29 @@ export interface BuildDispatcherOptions {
121
121
  devOnlyEnabled?: () => boolean;
122
122
  }
123
123
 
124
- export type Dispatch = (name: string, args: unknown, signal?: AbortSignal) => Promise<ToolResult>;
124
+ /**
125
+ * Call a tool by name.
126
+ *
127
+ * `ctx` is what the registry's `filter` (see `makeRegistry`) is evaluated
128
+ * against. From an MCP `tools/call` handler, pass the handler's `extra`:
129
+ * `dispatch(name, args, extra.signal, extra)`. An in-process caller omits it,
130
+ * and the filter sees `{}`.
131
+ */
132
+ export type Dispatch = (
133
+ name: string,
134
+ args: unknown,
135
+ signal?: AbortSignal,
136
+ ctx?: ToolFilterContext,
137
+ ) => Promise<ToolResult>;
125
138
 
126
139
  function defaultTimeoutMs(): number {
127
140
  return envNum("MCP_TOOL_TIMEOUT_DEFAULT_MS", 30_000);
128
141
  }
129
142
 
130
143
  function formatZodError(err: ZodError): string {
131
- return err.errors.map((e) => ` - ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
144
+ return err.issues
145
+ .map((e) => ` - ${e.path.map(String).join(".") || "(root)"}: ${e.message}`)
146
+ .join("\n");
132
147
  }
133
148
 
134
149
  export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
@@ -149,7 +164,7 @@ export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
149
164
  }
150
165
 
151
166
  const fallback = defaultTimeoutMs();
152
- return async (name, args, signal) => {
167
+ return async (name, args, signal, ctx = {}) => {
153
168
  noteActivity();
154
169
  opts.onCall?.(name);
155
170
  const def = opts.registry.get(name);
@@ -162,8 +177,12 @@ export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
162
177
  // construction, which the readonly type discourages but does not prevent.
163
178
  // If that ever happens the tool is gated, not exposed.
164
179
  const devGated = def?.devOnly === true && !(opts.devOnlyEnabled?.() ?? false);
180
+ // The registry's per-request filter gates the CALL path with the same
181
+ // predicate that gates the listing. Hiding a tool from tools/list while it
182
+ // still answers by name is the hole `devOnly` already fell into once.
183
+ const filtered = def !== undefined && !opts.registry.allows(def, ctx);
165
184
 
166
- if (!def || devGated) {
185
+ if (!def || devGated || filtered) {
167
186
  opts.onError?.(name, new Error("unknown_tool"));
168
187
  return {
169
188
  content: [
package/src/index.ts CHANGED
@@ -23,11 +23,17 @@ export { buildResourcesHandler } from "./resources.js";
23
23
  export { CONTENT_BUDGET, sanitize, sanitizeContent } from "./sanitize.js";
24
24
  export type {
25
25
  AnyToolDefinition,
26
+ AnyZodSchema,
26
27
  ContentBlock,
28
+ MakeRegistryOptions,
29
+ McpJsonSchema,
30
+ McpSchemaIo,
27
31
  ToolDefinition,
32
+ ToolFilter,
33
+ ToolFilterContext,
28
34
  ToolRegistry,
29
35
  } from "./tool-registry.js";
30
- export { makeRegistry } from "./tool-registry.js";
36
+ export { makeRegistry, toMcpSchema } from "./tool-registry.js";
31
37
  export type { HttpServerHandle, HttpServerOptions } from "./transports/http.js";
32
38
  export { startHttpServer } from "./transports/http.js";
33
39
  export type { StartStdioOptions } from "./transports/stdio.js";