@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 +72 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +75 -2
- package/dist/dispatch.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tool-registry.d.ts +89 -4
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/tool-registry.js +30 -5
- package/dist/tool-registry.js.map +1 -1
- package/dist/unknown-args.d.ts +39 -0
- package/dist/unknown-args.d.ts.map +1 -0
- package/dist/unknown-args.js +96 -0
- package/dist/unknown-args.js.map +1 -0
- package/package.json +1 -1
- package/src/dispatch.ts +88 -3
- package/src/index.ts +4 -0
- package/src/tool-registry.ts +119 -9
- package/src/unknown-args.ts +102 -0
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
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,OAAO,KAAK,
|
|
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
|
-
|
|
93
|
-
|
|
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);
|
package/dist/dispatch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACL,MAAM,EACN,KAAK,IAAI,QAAQ,EACjB,IAAI,IAAI,OAAO,EACf,YAAY,EACZ,IAAI,EACJ,gBAAgB,EAChB,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|
|
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"}
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
105
|
-
*
|
|
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) =>
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC7F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../src/tool-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/tool-registry.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/src/tool-registry.ts
CHANGED
|
@@ -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
|
-
*
|
|
113
|
-
*
|
|
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
|
-
) =>
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
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)
|
|
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
|
+
}
|