@george43g/mcp-kit 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,78 @@ server.setRequestHandler(CallToolRequestSchema, async (req, extra) =>
62
62
  means for your tools.
63
63
  - Synchronous, evaluated per request. **A throwing filter counts as `false`**.
64
64
 
65
+ ### Refusing with a reason: `{ deny }` (3.1.0)
66
+
67
+ `false` makes a tool look like it does not exist, which is right when its
68
+ existence is the secret and a dead end when the caller could fix the problem.
69
+ Return `{ deny: "reason" }` instead and the tool is hidden from `tools/list`
70
+ the same way, but a `tools/call` to it gets the reason:
71
+
72
+ ```ts
73
+ const registry = makeRegistry(tools, {
74
+ filter: (tool, ctx) =>
75
+ hasScopes(ctx, tool.scopes) ||
76
+ { deny: "not available. You may need to re-authenticate with additional scopes" },
77
+ });
78
+
79
+ await dispatch("gmail_send", { to: "…" });
80
+ // → { isError: true, content: [{ type: "text", text:
81
+ // 'Tool "gmail_send" failed: not available. You may need to re-authenticate with additional scopes' }] }
82
+ ```
83
+
84
+ - The reason is wrapped like every other dispatcher error
85
+ (`Tool "<name>" failed: <deny>`). Write it as a sentence an agent can act on.
86
+ - `false` keeps the unknown-tool wording, a throw still counts as `false`, and
87
+ so does `{ deny: "" }`: an empty reason tells the caller nothing.
88
+ - A gated `devOnly` tool answers as unknown even if the filter would deny it
89
+ with a reason; the filter is not consulted for it.
90
+ - `registry.allows()` still returns a boolean (`false` for `{ deny }`).
91
+ `registry.evaluate(def, ctx)` returns `{ allowed: true }` or
92
+ `{ allowed: false, deny? }`. It is optional on the `ToolRegistry` type, so a
93
+ hand-written 3.0 registry still compiles; the dispatcher falls back to
94
+ `allows()` for one, and its refusals then use the unknown-tool wording.
95
+
96
+ ## Strict arguments: `strictArgs` (3.1.0, off by default)
97
+
98
+ Zod strips an argument the schema does not declare. So when an agent misspells
99
+ an OPTIONAL argument, the call succeeds and the handler runs on the default as
100
+ if the argument had never been sent. Nothing tells the agent it was ignored.
101
+ `strictArgs` refuses the call instead, before parsing and before the handler:
102
+
103
+ ```ts
104
+ const registry = makeRegistry(tools, { strictArgs: true }); // every tool
105
+ const chat: ToolDefinition = { /* … */ strictArgs: false }; // a tool opts back out
106
+
107
+ await dispatch("chat_history", { chatIdentifer: "x", cursor: "c" });
108
+ // Tool "chat_history" failed: unknown argument chatIdentifer; did you mean chatIdentifier?
109
+ // unknown argument cursor
110
+ // Hint: Valid arguments: chatIdentifier, limit.
111
+ ```
112
+
113
+ - **Set it on the registry, override it per tool.** `ToolDefinition.strictArgs`
114
+ wins over `makeRegistry`'s `strictArgs` in either direction.
115
+ - **"Unknown" means "not in the advertised `inputSchema`'s top-level
116
+ `properties`."** So it behaves the same for a refined or transformed
117
+ `z.object` and for an intersection, which Zod merges into one object.
118
+ - **The advertised `inputSchema` does not change.** No `additionalProperties:
119
+ false` is added; clients see the same open schema whether this is on or off.
120
+ - **Valid input parses exactly as before.** The check only compares argument
121
+ names; parsing still runs the tool's own schema, so defaults and transforms
122
+ are untouched.
123
+ - **Top level only.** A nested `z.object` still strips unknown keys. To reject
124
+ them, declare that field `z.strictObject(…)`; Zod then reports it as
125
+ `Invalid arguments: … Unrecognized key: "frm"`, without a suggestion.
126
+ - **An input that declares itself open is not checked**: `z.looseObject`,
127
+ `.catchall()` or `z.record` at the top level has no unknown keys.
128
+ - A suggestion is offered when a declared name is within 2 edits (insertion,
129
+ deletion, substitution or swapping two adjacent letters), and those edits
130
+ are at most half its length. At most 20 unknown keys are named per error.
131
+ Echoed keys have control characters and newlines replaced.
132
+ - `registry.unknownArgs(def, args)` returns `[{ key, suggestion? }]`, and is
133
+ optional on the type for the same reason as `evaluate`. Without it, the
134
+ dispatcher still honours a tool's own `strictArgs: true`.
135
+ - The error text is rendered output and is not covered by semver.
136
+
65
137
  ## Testing: `@george43g/mcp-kit/testing`
66
138
 
67
139
  ```ts
@@ -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, ToolFilterContext, ToolRegistry } from "./tool-registry.js";
20
+ import { type ContentBlock, type ToolFilterContext, type ToolRegistry } from "./tool-registry.js";
21
21
  export interface ToolResult {
22
22
  content: ContentBlock[];
23
23
  structuredContent?: unknown;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAEtB,KAAK,YAAY,EAGlB,MAAM,oBAAoB,CAAC;AAiC5B,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,YAAY,CAAC;IACvB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,CACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,WAAW,EACpB,GAAG,CAAC,EAAE,iBAAiB,KACpB,OAAO,CAAC,UAAU,CAAC,CAAC;AA8CzB,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,CA2JtE"}
package/dist/dispatch.js CHANGED
@@ -21,6 +21,8 @@ import { homedir } from "node:os";
21
21
  import { envNum, error as logError, warn as logWarn, noteActivity, perf, ToolTimeoutError, withTimeout, } from "@george43g/robustness";
22
22
  import { wrapToolError } from "./prompt-injection.js";
23
23
  import { sanitize } from "./sanitize.js";
24
+ import { toMcpSchema, } from "./tool-registry.js";
25
+ import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
24
26
  /**
25
27
  * Replace the absolute home-directory prefix with `~` in anything headed for a
26
28
  * log sink.
@@ -59,6 +61,37 @@ function formatZodError(err) {
59
61
  .map((e) => ` - ${e.path.map(String).join(".") || "(root)"}: ${e.message}`)
60
62
  .join("\n");
61
63
  }
64
+ /** At most this many unknown keys are named in one error; the rest are counted. */
65
+ const MAX_UNKNOWN_REPORTED = 20;
66
+ function formatUnknownArgs(unknown) {
67
+ const lines = unknown.slice(0, MAX_UNKNOWN_REPORTED).map(({ key, suggestion }) => {
68
+ // The key is caller-supplied and echoed back: neutralise control characters,
69
+ // including the newlines sanitize() keeps, so a key cannot forge a line.
70
+ const shown = (sanitize(key, 128) ?? "").replace(/[\t\n\r]/g, "\uFFFD");
71
+ return suggestion === undefined
72
+ ? `unknown argument ${shown}`
73
+ : `unknown argument ${shown}; did you mean ${suggestion}?`;
74
+ });
75
+ const rest = unknown.length - MAX_UNKNOWN_REPORTED;
76
+ if (rest > 0)
77
+ lines.push(`and ${rest} more unknown argument(s)`);
78
+ return lines.join("\n");
79
+ }
80
+ // Declared top-level argument names per definition, derived from the wire
81
+ // input schema on first need. Weak so a definition can still be collected.
82
+ const declaredCache = new WeakMap();
83
+ function declaredFor(def) {
84
+ if (!declaredCache.has(def)) {
85
+ declaredCache.set(def, declaredArgs(toMcpSchema(def.input, "input", def.name)));
86
+ }
87
+ return declaredCache.get(def) ?? null;
88
+ }
89
+ function unknownArgsHint(def) {
90
+ const declared = declaredFor(def) ?? [];
91
+ return declared.length === 0
92
+ ? "This tool takes no arguments."
93
+ : `Valid arguments: ${declared.join(", ")}.`;
94
+ }
62
95
  export function buildDispatcher(opts) {
63
96
  // Make the ambiguous state unrepresentable, at construction, before a single
64
97
  // request can be served. See `devOnlyEnabled` above for why this is a throw
@@ -74,6 +107,21 @@ export function buildDispatcher(opts) {
74
107
  }
75
108
  }
76
109
  const fallback = defaultTimeoutMs();
110
+ const { registry } = opts;
111
+ // A registry built by `makeRegistry()` has both methods; a hand-written one
112
+ // from 3.0 may not, and still gets the boolean gate and per-tool strictArgs.
113
+ const evaluate = (def, ctx) => registry.evaluate
114
+ ? registry.evaluate(def, ctx)
115
+ : registry.allows(def, ctx)
116
+ ? { allowed: true }
117
+ : { allowed: false };
118
+ const unknownArgs = (def, args) => {
119
+ if (registry.unknownArgs)
120
+ return registry.unknownArgs(def, args);
121
+ if (def.strictArgs !== true)
122
+ return [];
123
+ return findUnknownArgs(declaredFor(def), args);
124
+ };
77
125
  return async (name, args, signal, ctx = {}) => {
78
126
  noteActivity();
79
127
  opts.onCall?.(name);
@@ -89,8 +137,18 @@ export function buildDispatcher(opts) {
89
137
  // The registry's per-request filter gates the CALL path with the same
90
138
  // predicate that gates the listing. Hiding a tool from tools/list while it
91
139
  // 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) {
140
+ // Evaluated only for a tool that exists and is not dev-gated, so a deny
141
+ // reason can never reveal a dev-only tool.
142
+ const decision = def !== undefined && !devGated ? evaluate(def, ctx) : { allowed: false };
143
+ // `{ deny }`: the filter chose to tell the caller why, so it can recover.
144
+ if (!decision.allowed && decision.deny !== undefined) {
145
+ opts.onError?.(name, new Error("tool_denied"));
146
+ return {
147
+ content: [{ type: "text", text: wrapToolError(name, decision.deny) }],
148
+ isError: true,
149
+ };
150
+ }
151
+ if (!def || !decision.allowed) {
94
152
  opts.onError?.(name, new Error("unknown_tool"));
95
153
  return {
96
154
  content: [
@@ -102,6 +160,21 @@ export function buildDispatcher(opts) {
102
160
  isError: true,
103
161
  };
104
162
  }
163
+ // strictArgs: refuse an undeclared argument name before parsing, because
164
+ // parsing would strip it silently and run the handler without it.
165
+ const unknown = unknownArgs(def, args ?? {});
166
+ if (unknown.length > 0) {
167
+ opts.onError?.(name, new Error("unknown_arguments"));
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: wrapToolError(name, formatUnknownArgs(unknown), unknownArgsHint(def)),
173
+ },
174
+ ],
175
+ isError: true,
176
+ };
177
+ }
105
178
  const parsed = def.input.safeParse(args ?? {});
106
179
  if (!parsed.success) {
107
180
  opts.onError?.(name, parsed.error);
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ 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, AnyZodSchema, ContentBlock, MakeRegistryOptions, McpJsonSchema, McpSchemaIo, ToolDefinition, ToolFilter, ToolFilterContext, ToolRegistry, } from "./tool-registry.js";
7
+ export type { AnyToolDefinition, AnyZodSchema, ContentBlock, MakeRegistryOptions, McpJsonSchema, McpSchemaIo, ToolDefinition, ToolDeny, ToolFilter, ToolFilterContext, ToolFilterDecision, ToolFilterResult, ToolRegistry, UnknownArgument, } from "./tool-registry.js";
8
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";
@@ -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,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,4BAA4B,EAC5B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,cAAc,EACd,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js.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;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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAEL,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAS/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAiB1E,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
@@ -62,6 +62,12 @@ export interface ToolDefinition<TInput extends z.ZodType = AnyZodSchema, TOutput
62
62
  timeoutMs?: number;
63
63
  /** When true, only register the tool if `MCP_DEV=1`. Used for `get_logs`. */
64
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;
65
71
  /** Handler: receives parsed input + AbortSignal, returns structured output. */
66
72
  handler: (input: z.infer<TInput>, signal?: AbortSignal) => Promise<z.infer<TOutput>>;
67
73
  /**
@@ -101,8 +107,49 @@ export interface ToolFilterContext {
101
107
  requestInfo?: RequestInfo;
102
108
  }
103
109
  /**
104
- * Per-request tool gate: return `false` to hide a tool from `tools/list` AND
105
- * refuse calls to it.
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" }`.
106
153
  *
107
154
  * Synchronous on purpose: it runs inside `toMcpTools()`, which is synchronous,
108
155
  * and a scope check is a set lookup. Resolve anything slow (a token
@@ -112,7 +159,7 @@ export interface ToolFilterContext {
112
159
  * A filter that THROWS is treated as `false` (fail closed): a bug in the gate
113
160
  * must not expose the tool it was written to protect.
114
161
  */
115
- export type ToolFilter<TDef extends AnyToolDefinition = AnyToolDefinition> = (tool: TDef, ctx: ToolFilterContext) => boolean;
162
+ export type ToolFilter<TDef extends AnyToolDefinition = AnyToolDefinition> = (tool: TDef, ctx: ToolFilterContext) => ToolFilterResult;
116
163
  /**
117
164
  * `TDef` lets a consumer carry its own per-tool metadata into the filter
118
165
  * without a cast: declare `interface ScopedTool extends ToolDefinition { scopes:
@@ -130,15 +177,53 @@ export interface MakeRegistryOptions<TDef extends AnyToolDefinition = AnyToolDef
130
177
  * from `tools/list` would leave it callable by name (see `devOnly`).
131
178
  */
132
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;
133
204
  }
134
205
  export interface ToolRegistry {
135
206
  readonly tools: readonly AnyToolDefinition[];
136
207
  get(name: string): AnyToolDefinition | undefined;
137
208
  /**
138
209
  * Whether `def` passes the registry's filter for this request. Always `true`
139
- * when no filter was configured; `false` when the filter throws.
210
+ * when no filter was configured; `false` when the filter returns `false` or
211
+ * `{ deny }`, or throws.
140
212
  */
141
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[];
142
227
  /**
143
228
  * Convert tools to MCP SDK shape: devOnly tools only when `includeDevOnly`,
144
229
  * and only tools the filter allows for `ctx`. Schemas are converted once, at
@@ -1 +1 @@
1
- {"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@
12
12
  * Schema path, so the wire contract cannot drift from the validator.
13
13
  */
14
14
  import { z } from "zod";
15
+ import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
15
16
  /**
16
17
  * Convert a Zod schema to the JSON Schema an MCP client accepts as a tool's
17
18
  * `inputSchema` / `outputSchema`.
@@ -54,6 +55,17 @@ export function toMcpSchema(schema, io, toolName) {
54
55
  }
55
56
  return json;
56
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
+ }
57
69
  export function makeRegistry(defs, opts = {}) {
58
70
  const byName = new Map(defs.map((d) => [d.name, d]));
59
71
  // Converted eagerly: a schema that cannot be represented throws HERE, at
@@ -70,25 +82,38 @@ export function makeRegistry(defs, opts = {}) {
70
82
  outputSchema: toMcpSchema(d.output, "output", d.name),
71
83
  },
72
84
  ]));
73
- const { filter } = opts;
74
- const allows = (def, ctx = {}) => {
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 = {}) => {
75
89
  if (!filter)
76
- return true;
90
+ return { allowed: true };
77
91
  try {
78
92
  // Only definitions from `defs` reach here in practice; the dispatcher
79
93
  // looks them up through `get()`.
80
- return filter(def, ctx) === true;
94
+ return decide(filter(def, ctx));
81
95
  }
82
96
  catch {
83
- return false;
97
+ return { allowed: false };
84
98
  }
85
99
  };
100
+ const allows = (def, ctx) => evaluate(def, ctx).allowed;
86
101
  return {
87
102
  tools: defs,
88
103
  get(name) {
89
104
  return byName.get(name);
90
105
  },
91
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
+ },
92
117
  toMcpTools(includeDevOnly = false, ctx = {}) {
93
118
  return defs
94
119
  .filter((d) => (includeDevOnly || !d.devOnly) && allows(d, ctx))
@@ -1 +1 @@
1
- {"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `strictArgs` support: which argument names a tool declares, which a call
3
+ * sent that it does not, and "did you mean …?" for each.
4
+ *
5
+ * Internal: nothing here is re-exported from the package. A general
6
+ * string-suggestion helper is not what mcp-kit is for, and every export is
7
+ * surface to keep stable.
8
+ */
9
+ import type { McpJsonSchema, UnknownArgument } from "./tool-registry.js";
10
+ /**
11
+ * Optimal-string-alignment distance: Levenshtein plus adjacent transposition,
12
+ * so `chatIdentifer` → `chatIdentifier` is 1 and `limti` → `limit` is 1.
13
+ *
14
+ * Returns `max + 1` as soon as the distance is known to exceed `max`, so a
15
+ * long unrelated key costs one row, not a full matrix.
16
+ */
17
+ export declare function editDistance(a: string, b: string, max?: number): number;
18
+ /**
19
+ * The closest candidate within 2 edits, or `undefined`.
20
+ *
21
+ * Two guards against noise: the distance must also be at most half the
22
+ * candidate's length (so `q` does not suggest `id`), and a tie goes to the
23
+ * candidate listed first, so the suggestion is deterministic.
24
+ */
25
+ export declare function closestMatch(input: string, candidates: Iterable<string>): string | undefined;
26
+ /**
27
+ * The top-level argument names an input wire schema declares, or `null` when
28
+ * the schema declares itself open (`additionalProperties` other than `false`,
29
+ * `patternProperties`, or `propertyNames`), in which case no name is unknown.
30
+ *
31
+ * Read from the wire schema rather than the Zod object so that "unknown" means
32
+ * exactly "not advertised to the client", for every Zod shape that converts to
33
+ * a top-level object: a refined or transformed `z.object`, and an intersection,
34
+ * which Zod 4 merges into one `properties` map.
35
+ */
36
+ export declare function declaredArgs(inputSchema: McpJsonSchema): readonly string[] | null;
37
+ /** Keys of `args` not in `declared`, each with its nearest declared name. */
38
+ export declare function findUnknownArgs(declared: readonly string[] | null, args: unknown): UnknownArgument[];
39
+ //# sourceMappingURL=unknown-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unknown-args.d.ts","sourceRoot":"","sources":["../src/unknown-args.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,SAA2B,GAAG,MAAM,CAqBzF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAY5F;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,CAWjF;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,EAClC,IAAI,EAAE,OAAO,GACZ,eAAe,EAAE,CAYnB"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `strictArgs` support: which argument names a tool declares, which a call
3
+ * sent that it does not, and "did you mean …?" for each.
4
+ *
5
+ * Internal: nothing here is re-exported from the package. A general
6
+ * string-suggestion helper is not what mcp-kit is for, and every export is
7
+ * surface to keep stable.
8
+ */
9
+ /**
10
+ * Optimal-string-alignment distance: Levenshtein plus adjacent transposition,
11
+ * so `chatIdentifer` → `chatIdentifier` is 1 and `limti` → `limit` is 1.
12
+ *
13
+ * Returns `max + 1` as soon as the distance is known to exceed `max`, so a
14
+ * long unrelated key costs one row, not a full matrix.
15
+ */
16
+ export function editDistance(a, b, max = Number.POSITIVE_INFINITY) {
17
+ if (Math.abs(a.length - b.length) > max)
18
+ return max + 1;
19
+ let prev2 = [];
20
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
21
+ for (let i = 1; i <= a.length; i++) {
22
+ const cur = [i];
23
+ let rowMin = i;
24
+ for (let j = 1; j <= b.length; j++) {
25
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
26
+ let d = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
27
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
28
+ d = Math.min(d, (prev2[j - 2] ?? 0) + 1);
29
+ }
30
+ cur[j] = d;
31
+ if (d < rowMin)
32
+ rowMin = d;
33
+ }
34
+ if (rowMin > max)
35
+ return max + 1;
36
+ prev2 = prev;
37
+ prev = cur;
38
+ }
39
+ return prev[b.length] ?? 0;
40
+ }
41
+ /**
42
+ * The closest candidate within 2 edits, or `undefined`.
43
+ *
44
+ * Two guards against noise: the distance must also be at most half the
45
+ * candidate's length (so `q` does not suggest `id`), and a tie goes to the
46
+ * candidate listed first, so the suggestion is deterministic.
47
+ */
48
+ export function closestMatch(input, candidates) {
49
+ let best;
50
+ let bestDistance = Number.POSITIVE_INFINITY;
51
+ for (const candidate of candidates) {
52
+ const limit = Math.min(2, Math.floor(candidate.length / 2));
53
+ const d = editDistance(input, candidate, limit);
54
+ if (d <= limit && d < bestDistance) {
55
+ best = candidate;
56
+ bestDistance = d;
57
+ }
58
+ }
59
+ return best;
60
+ }
61
+ /**
62
+ * The top-level argument names an input wire schema declares, or `null` when
63
+ * the schema declares itself open (`additionalProperties` other than `false`,
64
+ * `patternProperties`, or `propertyNames`), in which case no name is unknown.
65
+ *
66
+ * Read from the wire schema rather than the Zod object so that "unknown" means
67
+ * exactly "not advertised to the client", for every Zod shape that converts to
68
+ * a top-level object: a refined or transformed `z.object`, and an intersection,
69
+ * which Zod 4 merges into one `properties` map.
70
+ */
71
+ export function declaredArgs(inputSchema) {
72
+ const extra = inputSchema.additionalProperties;
73
+ if ((extra !== undefined && extra !== false) ||
74
+ inputSchema.patternProperties !== undefined ||
75
+ inputSchema.propertyNames !== undefined) {
76
+ return null;
77
+ }
78
+ const props = inputSchema.properties;
79
+ return props !== null && typeof props === "object" ? Object.keys(props) : [];
80
+ }
81
+ /** Keys of `args` not in `declared`, each with its nearest declared name. */
82
+ export function findUnknownArgs(declared, args) {
83
+ if (declared === null || args === null || typeof args !== "object" || Array.isArray(args)) {
84
+ return [];
85
+ }
86
+ const known = new Set(declared);
87
+ const out = [];
88
+ for (const key of Object.keys(args)) {
89
+ if (known.has(key))
90
+ continue;
91
+ const suggestion = closestMatch(key, declared);
92
+ out.push(suggestion === undefined ? { key } : { key, suggestion });
93
+ }
94
+ return out;
95
+ }
96
+ //# sourceMappingURL=unknown-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unknown-args.js","sourceRoot":"","sources":["../src/unknown-args.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": "3.0.0",
3
+ "version": "3.1.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",
package/src/dispatch.ts CHANGED
@@ -31,7 +31,16 @@ 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, ToolFilterContext, ToolRegistry } from "./tool-registry.js";
34
+ import {
35
+ type AnyToolDefinition,
36
+ type ContentBlock,
37
+ type ToolFilterContext,
38
+ type ToolFilterDecision,
39
+ type ToolRegistry,
40
+ toMcpSchema,
41
+ type UnknownArgument,
42
+ } from "./tool-registry.js";
43
+ import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
35
44
 
36
45
  /**
37
46
  * Replace the absolute home-directory prefix with `~` in anything headed for a
@@ -146,6 +155,40 @@ function formatZodError(err: ZodError): string {
146
155
  .join("\n");
147
156
  }
148
157
 
158
+ /** At most this many unknown keys are named in one error; the rest are counted. */
159
+ const MAX_UNKNOWN_REPORTED = 20;
160
+
161
+ function formatUnknownArgs(unknown: UnknownArgument[]): string {
162
+ const lines = unknown.slice(0, MAX_UNKNOWN_REPORTED).map(({ key, suggestion }) => {
163
+ // The key is caller-supplied and echoed back: neutralise control characters,
164
+ // including the newlines sanitize() keeps, so a key cannot forge a line.
165
+ const shown = (sanitize(key, 128) ?? "").replace(/[\t\n\r]/g, "\uFFFD");
166
+ return suggestion === undefined
167
+ ? `unknown argument ${shown}`
168
+ : `unknown argument ${shown}; did you mean ${suggestion}?`;
169
+ });
170
+ const rest = unknown.length - MAX_UNKNOWN_REPORTED;
171
+ if (rest > 0) lines.push(`and ${rest} more unknown argument(s)`);
172
+ return lines.join("\n");
173
+ }
174
+
175
+ // Declared top-level argument names per definition, derived from the wire
176
+ // input schema on first need. Weak so a definition can still be collected.
177
+ const declaredCache = new WeakMap<AnyToolDefinition, readonly string[] | null>();
178
+ function declaredFor(def: AnyToolDefinition): readonly string[] | null {
179
+ if (!declaredCache.has(def)) {
180
+ declaredCache.set(def, declaredArgs(toMcpSchema(def.input, "input", def.name)));
181
+ }
182
+ return declaredCache.get(def) ?? null;
183
+ }
184
+
185
+ function unknownArgsHint(def: AnyToolDefinition): string {
186
+ const declared = declaredFor(def) ?? [];
187
+ return declared.length === 0
188
+ ? "This tool takes no arguments."
189
+ : `Valid arguments: ${declared.join(", ")}.`;
190
+ }
191
+
149
192
  export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
150
193
  // Make the ambiguous state unrepresentable, at construction, before a single
151
194
  // request can be served. See `devOnlyEnabled` above for why this is a throw
@@ -164,6 +207,20 @@ export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
164
207
  }
165
208
 
166
209
  const fallback = defaultTimeoutMs();
210
+ const { registry } = opts;
211
+ // A registry built by `makeRegistry()` has both methods; a hand-written one
212
+ // from 3.0 may not, and still gets the boolean gate and per-tool strictArgs.
213
+ const evaluate = (def: AnyToolDefinition, ctx: ToolFilterContext): ToolFilterDecision =>
214
+ registry.evaluate
215
+ ? registry.evaluate(def, ctx)
216
+ : registry.allows(def, ctx)
217
+ ? { allowed: true }
218
+ : { allowed: false };
219
+ const unknownArgs = (def: AnyToolDefinition, args: unknown): UnknownArgument[] => {
220
+ if (registry.unknownArgs) return registry.unknownArgs(def, args);
221
+ if (def.strictArgs !== true) return [];
222
+ return findUnknownArgs(declaredFor(def), args);
223
+ };
167
224
  return async (name, args, signal, ctx = {}) => {
168
225
  noteActivity();
169
226
  opts.onCall?.(name);
@@ -180,9 +237,21 @@ export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
180
237
  // The registry's per-request filter gates the CALL path with the same
181
238
  // predicate that gates the listing. Hiding a tool from tools/list while it
182
239
  // still answers by name is the hole `devOnly` already fell into once.
183
- const filtered = def !== undefined && !opts.registry.allows(def, ctx);
240
+ // Evaluated only for a tool that exists and is not dev-gated, so a deny
241
+ // reason can never reveal a dev-only tool.
242
+ const decision: ToolFilterDecision =
243
+ def !== undefined && !devGated ? evaluate(def, ctx) : { allowed: false };
184
244
 
185
- if (!def || devGated || filtered) {
245
+ // `{ deny }`: the filter chose to tell the caller why, so it can recover.
246
+ if (!decision.allowed && decision.deny !== undefined) {
247
+ opts.onError?.(name, new Error("tool_denied"));
248
+ return {
249
+ content: [{ type: "text", text: wrapToolError(name, decision.deny) }],
250
+ isError: true,
251
+ };
252
+ }
253
+
254
+ if (!def || !decision.allowed) {
186
255
  opts.onError?.(name, new Error("unknown_tool"));
187
256
  return {
188
257
  content: [
@@ -195,6 +264,22 @@ export function buildDispatcher(opts: BuildDispatcherOptions): Dispatch {
195
264
  };
196
265
  }
197
266
 
267
+ // strictArgs: refuse an undeclared argument name before parsing, because
268
+ // parsing would strip it silently and run the handler without it.
269
+ const unknown = unknownArgs(def, args ?? {});
270
+ if (unknown.length > 0) {
271
+ opts.onError?.(name, new Error("unknown_arguments"));
272
+ return {
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: wrapToolError(name, formatUnknownArgs(unknown), unknownArgsHint(def)),
277
+ },
278
+ ],
279
+ isError: true,
280
+ };
281
+ }
282
+
198
283
  const parsed = def.input.safeParse(args ?? {});
199
284
  if (!parsed.success) {
200
285
  opts.onError?.(name, parsed.error);
package/src/index.ts CHANGED
@@ -29,9 +29,13 @@ export type {
29
29
  McpJsonSchema,
30
30
  McpSchemaIo,
31
31
  ToolDefinition,
32
+ ToolDeny,
32
33
  ToolFilter,
33
34
  ToolFilterContext,
35
+ ToolFilterDecision,
36
+ ToolFilterResult,
34
37
  ToolRegistry,
38
+ UnknownArgument,
35
39
  } from "./tool-registry.js";
36
40
  export { makeRegistry, toMcpSchema } from "./tool-registry.js";
37
41
  export type { HttpServerHandle, HttpServerOptions } from "./transports/http.js";
@@ -15,6 +15,7 @@
15
15
  import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
16
16
  import type { RequestInfo, Tool, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
17
17
  import { z } from "zod";
18
+ import { declaredArgs, findUnknownArgs } from "./unknown-args.js";
18
19
 
19
20
  /**
20
21
  * A single MCP content block a tool may emit.
@@ -63,6 +64,12 @@ export interface ToolDefinition<
63
64
  timeoutMs?: number;
64
65
  /** When true, only register the tool if `MCP_DEV=1`. Used for `get_logs`. */
65
66
  devOnly?: boolean;
67
+ /**
68
+ * Reject a call that carries an argument name this tool does not declare,
69
+ * before the handler runs. Overrides the registry's `strictArgs` for this
70
+ * tool, in either direction. See `MakeRegistryOptions.strictArgs`.
71
+ */
72
+ strictArgs?: boolean;
66
73
  /** Handler: receives parsed input + AbortSignal, returns structured output. */
67
74
  handler: (input: z.infer<TInput>, signal?: AbortSignal) => Promise<z.infer<TOutput>>;
68
75
  /**
@@ -109,8 +116,48 @@ export interface ToolFilterContext {
109
116
  }
110
117
 
111
118
  /**
112
- * Per-request tool gate: return `false` to hide a tool from `tools/list` AND
113
- * refuse calls to it.
119
+ * A filter's refusal WITH a reason the caller may see. Returned instead of
120
+ * `false` when the caller can do something about it (re-authenticate, ask for
121
+ * a scope), so the refusal is recoverable instead of a dead end.
122
+ */
123
+ export interface ToolDeny {
124
+ /**
125
+ * Shown to the caller as the error text of a `tools/call` to the tool,
126
+ * wrapped like every other dispatcher error:
127
+ * `Tool "<name>" failed: <deny>`. Must be non-empty; an empty string is
128
+ * treated as a plain `false`.
129
+ */
130
+ deny: string;
131
+ }
132
+
133
+ /** What a `ToolFilter` may return. */
134
+ export type ToolFilterResult = boolean | ToolDeny;
135
+
136
+ /**
137
+ * The registry's verdict on one tool for one request, from
138
+ * `ToolRegistry.evaluate`. `deny` is present only when the filter returned
139
+ * `{ deny }`; a plain `false`, a throw, or any other value has none, and the
140
+ * dispatcher then answers exactly as for an unknown tool.
141
+ */
142
+ export type ToolFilterDecision = { allowed: true } | { allowed: false; deny?: string };
143
+
144
+ /** One argument name a strict tool does not declare, from `ToolRegistry.unknownArgs`. */
145
+ export interface UnknownArgument {
146
+ /** The name as the caller sent it. */
147
+ key: string;
148
+ /** The declared argument it most likely meant (within 2 edits), if any. */
149
+ suggestion?: string;
150
+ }
151
+
152
+ /**
153
+ * Per-request tool gate. Return `true` to allow the tool, `false` to hide it
154
+ * from `tools/list` AND refuse calls to it as if it did not exist, or
155
+ * `{ deny: "reason" }` to hide it the same way but refuse a call with that
156
+ * reason instead of the unknown-tool wording.
157
+ *
158
+ * `false` is the right answer when the tool's existence is itself the secret;
159
+ * `{ deny }` when the caller can recover, e.g. gmail's
160
+ * `{ deny: "not available. You may need to re-authenticate with additional scopes" }`.
114
161
  *
115
162
  * Synchronous on purpose: it runs inside `toMcpTools()`, which is synchronous,
116
163
  * and a scope check is a set lookup. Resolve anything slow (a token
@@ -123,7 +170,7 @@ export interface ToolFilterContext {
123
170
  export type ToolFilter<TDef extends AnyToolDefinition = AnyToolDefinition> = (
124
171
  tool: TDef,
125
172
  ctx: ToolFilterContext,
126
- ) => boolean;
173
+ ) => ToolFilterResult;
127
174
 
128
175
  /**
129
176
  * `TDef` lets a consumer carry its own per-tool metadata into the filter
@@ -142,6 +189,30 @@ export interface MakeRegistryOptions<TDef extends AnyToolDefinition = AnyToolDef
142
189
  * from `tools/list` would leave it callable by name (see `devOnly`).
143
190
  */
144
191
  filter?: ToolFilter<TDef>;
192
+ /**
193
+ * Default for every tool's `strictArgs`; a tool's own `strictArgs` wins.
194
+ * Off unless set.
195
+ *
196
+ * When on, a call that names an argument the tool's advertised `inputSchema`
197
+ * does not list under its top-level `properties` is refused BEFORE parsing
198
+ * and before the handler, naming each unknown key with a "did you mean"
199
+ * suggestion when a declared key is within 2 edits:
200
+ * `unknown argument chatIdentifer; did you mean chatIdentifier?`.
201
+ * When off, Zod strips an unknown key silently and the handler runs as if
202
+ * the argument had not been sent, which is how a misspelled optional
203
+ * argument quietly falls back to its default.
204
+ *
205
+ * - The advertised `inputSchema` is unchanged: no `additionalProperties:
206
+ * false` is added, so clients see the same open schema either way.
207
+ * - Valid input parses exactly as without it: the check only reads the
208
+ * argument names, and parsing still runs the tool's own schema.
209
+ * - Top level only. A nested `z.object` still strips unknown keys; declare
210
+ * it `z.strictObject` to reject them (Zod's own error, no suggestion).
211
+ * - A tool whose input declares itself open at the top level
212
+ * (`z.looseObject`, `.catchall()`, `z.record`) has no unknown keys, and
213
+ * the option does nothing for it.
214
+ */
215
+ strictArgs?: boolean;
145
216
  }
146
217
 
147
218
  export interface ToolRegistry {
@@ -149,9 +220,23 @@ export interface ToolRegistry {
149
220
  get(name: string): AnyToolDefinition | undefined;
150
221
  /**
151
222
  * Whether `def` passes the registry's filter for this request. Always `true`
152
- * when no filter was configured; `false` when the filter throws.
223
+ * when no filter was configured; `false` when the filter returns `false` or
224
+ * `{ deny }`, or throws.
153
225
  */
154
226
  allows(def: AnyToolDefinition, ctx?: ToolFilterContext): boolean;
227
+ /**
228
+ * `allows`, plus the filter's `{ deny }` reason. Optional so a hand-written
229
+ * registry from 3.0 still type-checks; `makeRegistry()` always provides it,
230
+ * and the dispatcher falls back to `allows` without it.
231
+ */
232
+ evaluate?(def: AnyToolDefinition, ctx?: ToolFilterContext): ToolFilterDecision;
233
+ /**
234
+ * The argument names in `args` that `def` does not declare, when `def` is
235
+ * strict (its own `strictArgs`, else the registry's); `[]` otherwise, and
236
+ * `[]` when `args` is not a plain object. Optional for the same reason as
237
+ * `evaluate`; without it the dispatcher honours a tool's own `strictArgs`.
238
+ */
239
+ unknownArgs?(def: AnyToolDefinition, args: unknown): UnknownArgument[];
155
240
  /**
156
241
  * Convert tools to MCP SDK shape: devOnly tools only when `includeDevOnly`,
157
242
  * and only tools the filter allows for `ctx`. Schemas are converted once, at
@@ -216,6 +301,16 @@ export function toMcpSchema(schema: z.ZodType, io: McpSchemaIo, toolName?: strin
216
301
  return json as McpJsonSchema;
217
302
  }
218
303
 
304
+ /** Read a filter's return value. Anything but `true` or a non-empty `{ deny }` is a plain refusal. */
305
+ function decide(result: unknown): ToolFilterDecision {
306
+ if (result === true) return { allowed: true };
307
+ if (result !== null && typeof result === "object") {
308
+ const deny = (result as { deny?: unknown }).deny;
309
+ if (typeof deny === "string" && deny.trim() !== "") return { allowed: false, deny };
310
+ }
311
+ return { allowed: false };
312
+ }
313
+
219
314
  export function makeRegistry<TDef extends AnyToolDefinition = AnyToolDefinition>(
220
315
  defs: TDef[],
221
316
  opts: MakeRegistryOptions<TDef> = {},
@@ -237,23 +332,38 @@ export function makeRegistry<TDef extends AnyToolDefinition = AnyToolDefinition>
237
332
  } as unknown as Tool,
238
333
  ]),
239
334
  );
240
- const { filter } = opts;
241
- const allows = (def: AnyToolDefinition, ctx: ToolFilterContext = {}): boolean => {
242
- if (!filter) return true;
335
+ const { filter, strictArgs = false } = opts;
336
+ // Derived from the wire schema computed above, once per definition.
337
+ const declared = new Map(
338
+ defs.map((d) => [d, declaredArgs((wire.get(d) as Tool).inputSchema as McpJsonSchema)]),
339
+ );
340
+ const evaluate = (def: AnyToolDefinition, ctx: ToolFilterContext = {}): ToolFilterDecision => {
341
+ if (!filter) return { allowed: true };
243
342
  try {
244
343
  // Only definitions from `defs` reach here in practice; the dispatcher
245
344
  // looks them up through `get()`.
246
- return filter(def as TDef, ctx) === true;
345
+ return decide(filter(def as TDef, ctx));
247
346
  } catch {
248
- return false;
347
+ return { allowed: false };
249
348
  }
250
349
  };
350
+ const allows = (def: AnyToolDefinition, ctx?: ToolFilterContext): boolean =>
351
+ evaluate(def, ctx).allowed;
251
352
  return {
252
353
  tools: defs,
253
354
  get(name) {
254
355
  return byName.get(name);
255
356
  },
256
357
  allows,
358
+ evaluate,
359
+ unknownArgs(def, args) {
360
+ if (!(def.strictArgs ?? strictArgs)) return [];
361
+ // A definition from outside `defs` has no precomputed entry; derive it.
362
+ const keys = declared.has(def as TDef)
363
+ ? (declared.get(def as TDef) ?? null)
364
+ : declaredArgs(toMcpSchema(def.input, "input", def.name));
365
+ return findUnknownArgs(keys, args);
366
+ },
257
367
  toMcpTools(includeDevOnly = false, ctx: ToolFilterContext = {}): Tool[] {
258
368
  return defs
259
369
  .filter((d) => (includeDevOnly || !d.devOnly) && allows(d, ctx))
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `strictArgs` support: which argument names a tool declares, which a call
3
+ * sent that it does not, and "did you mean …?" for each.
4
+ *
5
+ * Internal: nothing here is re-exported from the package. A general
6
+ * string-suggestion helper is not what mcp-kit is for, and every export is
7
+ * surface to keep stable.
8
+ */
9
+
10
+ import type { McpJsonSchema, UnknownArgument } from "./tool-registry.js";
11
+
12
+ /**
13
+ * Optimal-string-alignment distance: Levenshtein plus adjacent transposition,
14
+ * so `chatIdentifer` → `chatIdentifier` is 1 and `limti` → `limit` is 1.
15
+ *
16
+ * Returns `max + 1` as soon as the distance is known to exceed `max`, so a
17
+ * long unrelated key costs one row, not a full matrix.
18
+ */
19
+ export function editDistance(a: string, b: string, max = Number.POSITIVE_INFINITY): number {
20
+ if (Math.abs(a.length - b.length) > max) return max + 1;
21
+ let prev2: number[] = [];
22
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
23
+ for (let i = 1; i <= a.length; i++) {
24
+ const cur = [i];
25
+ let rowMin = i;
26
+ for (let j = 1; j <= b.length; j++) {
27
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
28
+ let d = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
29
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
30
+ d = Math.min(d, (prev2[j - 2] ?? 0) + 1);
31
+ }
32
+ cur[j] = d;
33
+ if (d < rowMin) rowMin = d;
34
+ }
35
+ if (rowMin > max) return max + 1;
36
+ prev2 = prev;
37
+ prev = cur;
38
+ }
39
+ return prev[b.length] ?? 0;
40
+ }
41
+
42
+ /**
43
+ * The closest candidate within 2 edits, or `undefined`.
44
+ *
45
+ * Two guards against noise: the distance must also be at most half the
46
+ * candidate's length (so `q` does not suggest `id`), and a tie goes to the
47
+ * candidate listed first, so the suggestion is deterministic.
48
+ */
49
+ export function closestMatch(input: string, candidates: Iterable<string>): string | undefined {
50
+ let best: string | undefined;
51
+ let bestDistance = Number.POSITIVE_INFINITY;
52
+ for (const candidate of candidates) {
53
+ const limit = Math.min(2, Math.floor(candidate.length / 2));
54
+ const d = editDistance(input, candidate, limit);
55
+ if (d <= limit && d < bestDistance) {
56
+ best = candidate;
57
+ bestDistance = d;
58
+ }
59
+ }
60
+ return best;
61
+ }
62
+
63
+ /**
64
+ * The top-level argument names an input wire schema declares, or `null` when
65
+ * the schema declares itself open (`additionalProperties` other than `false`,
66
+ * `patternProperties`, or `propertyNames`), in which case no name is unknown.
67
+ *
68
+ * Read from the wire schema rather than the Zod object so that "unknown" means
69
+ * exactly "not advertised to the client", for every Zod shape that converts to
70
+ * a top-level object: a refined or transformed `z.object`, and an intersection,
71
+ * which Zod 4 merges into one `properties` map.
72
+ */
73
+ export function declaredArgs(inputSchema: McpJsonSchema): readonly string[] | null {
74
+ const extra = inputSchema.additionalProperties;
75
+ if (
76
+ (extra !== undefined && extra !== false) ||
77
+ inputSchema.patternProperties !== undefined ||
78
+ inputSchema.propertyNames !== undefined
79
+ ) {
80
+ return null;
81
+ }
82
+ const props = inputSchema.properties;
83
+ return props !== null && typeof props === "object" ? Object.keys(props) : [];
84
+ }
85
+
86
+ /** Keys of `args` not in `declared`, each with its nearest declared name. */
87
+ export function findUnknownArgs(
88
+ declared: readonly string[] | null,
89
+ args: unknown,
90
+ ): UnknownArgument[] {
91
+ if (declared === null || args === null || typeof args !== "object" || Array.isArray(args)) {
92
+ return [];
93
+ }
94
+ const known = new Set(declared);
95
+ const out: UnknownArgument[] = [];
96
+ for (const key of Object.keys(args)) {
97
+ if (known.has(key)) continue;
98
+ const suggestion = closestMatch(key, declared);
99
+ out.push(suggestion === undefined ? { key } : { key, suggestion });
100
+ }
101
+ return out;
102
+ }