@effected/mcp 0.2.0 → 0.4.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/McpHarness.js +24 -8
- package/McpProbe.js +16 -3
- package/McpProcess.js +19 -2
- package/McpStdio.js +30 -22
- package/McpTestFailure.js +4 -2
- package/McpToolAudit.js +5 -3
- package/McpToolkit.js +125 -76
- package/README.md +79 -35
- package/ToolFailure.js +4 -1
- package/ToolInputSchema.js +28 -14
- package/ToolRefusal.js +5 -2
- package/index.d.ts +125 -84
- package/internal/LaunchFailed.js +15 -3
- package/internal/StdinFrames.js +61 -23
- package/package.json +4 -4
- package/testing.d.ts +61 -13
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# @effected/mcp
|
|
2
2
|
|
|
3
|
-
The boundary layer of an `effect/
|
|
3
|
+
The boundary layer of an `effect/ai` MCP server: stdio wiring that keeps stdout the JSON-RPC wire, tool-failure shaping, and strict-input walkers, plus a `./testing` subpath for driving a built server from a test.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@effected/mcp)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
-
> **Pre
|
|
11
|
-
>
|
|
12
|
-
> `1.0.0`
|
|
13
|
-
>
|
|
10
|
+
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
+
> Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
|
|
12
|
+
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
|
+
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
14
|
> [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
|
|
15
15
|
>
|
|
16
16
|
> **Stability: unstable.** This package's API surface is not yet considered
|
|
@@ -94,7 +94,7 @@ For a refusal with no fields of its own, `ToolRefusal` is that shape ready-made:
|
|
|
94
94
|
```ts
|
|
95
95
|
import { ToolFailure, ToolRefusal } from "@effected/mcp";
|
|
96
96
|
import { Effect, Schema } from "effect";
|
|
97
|
-
import { Tool } from "effect/
|
|
97
|
+
import { Tool } from "effect/ai";
|
|
98
98
|
|
|
99
99
|
const GetRun = Tool.make("get_run", {
|
|
100
100
|
parameters: Schema.Struct({ id: Schema.String }),
|
|
@@ -120,7 +120,7 @@ A stdio server's `main.ts` is one line, plus the layer that wires it:
|
|
|
120
120
|
import { McpStdio, McpToolkit, ToolFailure } from "@effected/mcp";
|
|
121
121
|
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
122
122
|
import { Effect, Layer, Schema } from "effect";
|
|
123
|
-
import { Tool, Toolkit } from "effect/
|
|
123
|
+
import { Tool, Toolkit } from "effect/ai";
|
|
124
124
|
|
|
125
125
|
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
|
|
126
126
|
...ToolFailure.fields,
|
|
@@ -172,20 +172,20 @@ its own queue-backed `Stdio`.
|
|
|
172
172
|
|
|
173
173
|
`McpToolkit.layer` re-annotates every tool without its own `Tool.Strict`
|
|
174
174
|
annotation to strict by default, so `get_thing({ id: "known", extra: 1 })`
|
|
175
|
-
is rejected
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
is rejected by core's strict decode, which names every excess, missing and
|
|
176
|
+
invalid field in one `InvalidParams`, before the handler ever runs, and the
|
|
177
|
+
layer adds `Accepted params at the root: id.` — see
|
|
178
|
+
[Strict input](#strict-input) below.
|
|
178
179
|
|
|
179
180
|
## The stdio boundary
|
|
180
181
|
|
|
181
182
|
`McpStdio.layer` is core's `McpServer.layerStdio` with every log line sent to
|
|
182
|
-
stderr, and with stdin read through a guard. Core's own stdio decoder
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
stdout:
|
|
183
|
+
stderr, and with stdin read through a guard. Core's own stdio decoder skips a
|
|
184
|
+
line it cannot use and keeps serving, but it sends no reply, where JSON-RPC 2.0
|
|
185
|
+
requires one: the client is left waiting on a request it will never hear about.
|
|
186
|
+
The guard frames stdin exactly as core does — one streaming UTF-8 decoder, a
|
|
187
|
+
byte-order mark stripped only at the start of the stream, lines split on `\n`
|
|
188
|
+
— and answers each such line itself, on stdout:
|
|
189
189
|
|
|
190
190
|
- a line that is not JSON gets a JSON-RPC parse error,
|
|
191
191
|
`{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}`,
|
|
@@ -196,11 +196,16 @@ stdout:
|
|
|
196
196
|
- a line that is JSON but no JSON-RPC message gets an Invalid Request,
|
|
197
197
|
`{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid Request"}}`,
|
|
198
198
|
and the server keeps serving. That covers a value that is neither an object
|
|
199
|
-
nor an array
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`id
|
|
199
|
+
nor an array, an object whose `method` is not a string and whose `id` is
|
|
200
|
+
absent or `null`, and an object with neither `method` nor `id`, which is
|
|
201
|
+
neither a request nor a response;
|
|
202
|
+
- a line whose `method` starts with `@effect/rpc/` never reaches core, which
|
|
203
|
+
would read one without an `id` as its own RPC control message:
|
|
204
|
+
`{"jsonrpc":"2.0","method":"@effect/rpc/Eof"}` alone silently stops an
|
|
205
|
+
unguarded server ([Effect-TS/effect#8499](https://github.com/Effect-TS/effect/issues/8499),
|
|
206
|
+
open upstream). Such a notification is
|
|
207
|
+
dropped; such a request is answered `-32601` Method not found, echoing its
|
|
208
|
+
`id`;
|
|
204
209
|
- a line of JSON whitespace (space, tab, carriage return) is ignored.
|
|
205
210
|
|
|
206
211
|
Every other line goes to core. That includes an array, which core answers with
|
|
@@ -275,35 +280,73 @@ scopes:
|
|
|
275
280
|
- **`McpToolkit.layer(toolkit, options?)`** — the registration-scoped
|
|
276
281
|
decorator, and the recommended default: every tool without its own
|
|
277
282
|
`Tool.Strict` annotation is served and decoded strict
|
|
278
|
-
(`options.strict` defaults to `"all"`)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
283
|
+
(`options.strict` defaults to `"all"`). Rejection and the report are
|
|
284
|
+
core's: a strict tool is decoded with `errors: "all"`, so one
|
|
285
|
+
`InvalidParams` names every excess key at every depth together with every
|
|
286
|
+
missing or invalid field. The layer appends to that report, never in
|
|
287
|
+
place of it: one `Accepted params at <path>: …` line for each object level
|
|
288
|
+
of the payload that carries an unknown key, so an agent can fix the call
|
|
289
|
+
from the reply alone (example below). An explicit `Tool.Strict`
|
|
290
|
+
annotation always wins, and a `Tool.dynamic` tool is never re-annotated.
|
|
291
|
+
`options.unknownKeyMessage` is deprecated and ignored. Pass `{ strict: "annotated" }` to leave
|
|
282
292
|
unannotated tools lenient, or annotate an individual tool
|
|
283
293
|
`Tool.Strict` false to opt it out under the default.
|
|
284
294
|
|
|
285
295
|
```ts
|
|
286
296
|
import { McpToolkit } from "@effected/mcp";
|
|
287
297
|
import { Layer } from "effect";
|
|
288
|
-
import type { Tool, Toolkit } from "effect/
|
|
298
|
+
import type { Tool, Toolkit } from "effect/ai";
|
|
289
299
|
|
|
290
300
|
// The toolkit and its handlers, as built in "Putting it together".
|
|
291
301
|
declare const MyTools: Toolkit.Toolkit<Record<string, Tool.Any>>;
|
|
292
302
|
declare const MyHandlers: Layer.Layer<never>;
|
|
293
303
|
|
|
294
|
-
// Default: every tool is strict
|
|
304
|
+
// Default: every tool is strict; core's rejection names every bad field at once.
|
|
295
305
|
const ToolsLayer = McpToolkit.layer(MyTools).pipe(Layer.provide(MyHandlers));
|
|
296
306
|
|
|
297
307
|
// Only tools explicitly annotated Tool.Strict are decoded strict.
|
|
298
308
|
const LenientByDefault = McpToolkit.layer(MyTools, { strict: "annotated" }).pipe(Layer.provide(MyHandlers));
|
|
299
309
|
```
|
|
300
310
|
|
|
311
|
+
A strict `search` tool taking `query` and an optional `filter` union, called
|
|
312
|
+
with an unknown key at the root and one inside the selected `filter` member,
|
|
313
|
+
and without its required `query`:
|
|
314
|
+
|
|
315
|
+
```text
|
|
316
|
+
search({ extra: 1, filter: { kind: "tag", tag: "t", text: "sneaky" } })
|
|
317
|
+
=> Invalid parameters for tool 'search': Expected no excess property
|
|
318
|
+
at ["extra"]
|
|
319
|
+
Missing key
|
|
320
|
+
at ["query"]
|
|
321
|
+
Expected no excess property
|
|
322
|
+
at ["filter"]["text"]
|
|
323
|
+
Accepted params at the root: query, filter.
|
|
324
|
+
Accepted params at ["filter"]: kind, tag.
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The first six lines are core's. The layer adds the last two: one per object
|
|
328
|
+
level that carries an unknown key, naming its accepted keys, plus
|
|
329
|
+
`keys matching <pattern>` where the level accepts keys by `patternProperties`.
|
|
330
|
+
A zero-parameter tool gets `This tool accepts no params.` instead. A failure
|
|
331
|
+
with no unknown key, only missing or mistyped fields, is core's report
|
|
332
|
+
unchanged.
|
|
333
|
+
|
|
334
|
+
A pattern-keyed `Schema.Record` is served closed, with `patternProperties`,
|
|
335
|
+
only when its key pattern's RegExp carries the Unicode flag:
|
|
336
|
+
`Schema.isPattern(/^x-/u)`. Without `u`, Effect treats the exported pattern as
|
|
337
|
+
approximate and serves the record open (`additionalProperties` set to the value
|
|
338
|
+
schema), so the served schema, and `ToolInputSchema.unknownKeys` walking it,
|
|
339
|
+
accept any key; the strict decode still enforces the pattern. `McpToolAudit`'s `input: "closed"` check reports such
|
|
340
|
+
a record open and suggests the flag. Where a closed level accepts keys by
|
|
341
|
+
pattern, `formatUnknownKeys` names the patterns (`Accepted keys matching: ^x-.`)
|
|
342
|
+
rather than claiming it accepts none.
|
|
343
|
+
|
|
301
344
|
The dynamic-tool recipe, with the handler reporting every unknown key:
|
|
302
345
|
|
|
303
346
|
```ts
|
|
304
347
|
import { McpToolkit, ToolInputSchema } from "@effected/mcp";
|
|
305
348
|
import { Effect, Layer, Schema } from "effect";
|
|
306
|
-
import { Tool, Toolkit } from "effect/
|
|
349
|
+
import { Tool, Toolkit } from "effect/ai";
|
|
307
350
|
|
|
308
351
|
// A top-level union, written as raw JSON Schema and rewritten to an object root.
|
|
309
352
|
const EditInput = ToolInputSchema.objectRooted({
|
|
@@ -363,7 +406,7 @@ A tool whose parameters are a `Schema.Union` of objects cannot be a `Tool.make`
|
|
|
363
406
|
```ts
|
|
364
407
|
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
365
408
|
import { Effect, Schema } from "effect";
|
|
366
|
-
import { Tool, Toolkit } from "effect/
|
|
409
|
+
import { Tool, Toolkit } from "effect/ai";
|
|
367
410
|
|
|
368
411
|
const AddNote = Schema.Struct({ action: Schema.Literal("add"), text: Schema.String });
|
|
369
412
|
const ListNotes = Schema.Struct({ action: Schema.Literal("list"), limit: Schema.optionalKey(Schema.Number) });
|
|
@@ -391,7 +434,7 @@ const Handlers = Kit.toLayer({
|
|
|
391
434
|
|
|
392
435
|
`annotate` and `addDependency` keep the union on the tool, so a chained `.annotate(Tool.Title, …)` still hands `unionHandler` the decoded type.
|
|
393
436
|
|
|
394
|
-
Registered through `McpToolkit.layer`, a union tool is
|
|
437
|
+
Registered through `McpToolkit.layer`, a union tool is decoded like a strict `Tool.make` tool, before the handler runs: strictly, with `errors: "all"`, so one `InvalidParams` in core's `Tool.make` wording names every excess key at every depth together with every missing or invalid field of the matched member, followed by the same `Accepted params at <path>: …` lines. Bad arguments therefore answer as a `Tool.make` decode failure does: a JSON-RPC `-32602` on `2025-06-18`, an `isError` result on the later revisions. Registered through core's `McpServer.toolkit` instead, or called directly (a test helper, say), `unionHandler` runs the same decode itself, so the message is identical. Under `McpServer.toolkit` that `InvalidParams` is a declared failure, an `isError` result on every revision. `unionHandler`'s third argument, `{ unknownKeyMessage }`, is deprecated and ignored. Never annotate a union tool `Tool.Strict` true: core dies at registration on a strict tool with a raw JSON Schema.
|
|
395
438
|
|
|
396
439
|
## Union success schemas
|
|
397
440
|
|
|
@@ -417,7 +460,7 @@ test machinery into a server's runtime import graph.
|
|
|
417
460
|
```ts
|
|
418
461
|
import { McpHarness } from "@effected/mcp/testing";
|
|
419
462
|
import { Effect, type Layer, type Stdio } from "effect";
|
|
420
|
-
import type { McpServer } from "effect/
|
|
463
|
+
import type { McpServer } from "effect/ai";
|
|
421
464
|
|
|
422
465
|
// The server layer from "Putting it together" — still requiring Stdio.
|
|
423
466
|
declare const ServerLayer: Layer.Layer<McpServer.McpServer, never, Stdio.Stdio>;
|
|
@@ -434,7 +477,8 @@ const test = Effect.gen(function* () {
|
|
|
434
477
|
On a stateful revision (the default is `2025-11-25`), `initialize` comes
|
|
435
478
|
first: any other request sent before it fails fast with `McpTestFailure`
|
|
436
479
|
reason `NotInitialized` instead of the server's opaque `Invalid request
|
|
437
|
-
metadata`. `
|
|
480
|
+
metadata`. `ping` is the exception: the server answers it `{}` before
|
|
481
|
+
initialization, so the harness sends it. `client.initializeWith("2025-06-18")` asks for a different stateful revision than the harness speaks and returns the whole response, so a test can assert the negotiated `protocolVersion`. A stateful server never refuses a version: an unknown one, or the stateless `2026-07-28`, is counter-offered `2025-11-25`. On a harness made with the stateless revision, `initialize` itself is not served, and the response is a JSON-RPC `-32601` error with no `notifications/initialized` sent after it. `client.sentSoFar` holds every frame the harness has written, in order.
|
|
438
482
|
|
|
439
483
|
`McpHarness.make` runs the server in-process over queue-backed `Stdio`, so
|
|
440
484
|
a test sees the exact served schemas and wire results a real client would,
|
|
@@ -477,7 +521,7 @@ import { join } from "node:path";
|
|
|
477
521
|
import { NodeServices } from "@effect/platform-node";
|
|
478
522
|
import { McpProcess } from "@effected/mcp/testing";
|
|
479
523
|
import { Effect } from "effect";
|
|
480
|
-
import { ChildProcess } from "effect/
|
|
524
|
+
import { ChildProcess } from "effect/process";
|
|
481
525
|
|
|
482
526
|
// A minimal server, written to disk so it can be spawned as a real process.
|
|
483
527
|
const serverFile = join(import.meta.dirname, "mcp-guard-demo-server.mjs");
|
|
@@ -487,7 +531,7 @@ writeFileSync(
|
|
|
487
531
|
import { McpStdio, McpToolkit } from "@effected/mcp";
|
|
488
532
|
import { NodeRuntime, NodeStdio } from "@effect/platform-node";
|
|
489
533
|
import { Effect, Layer } from "effect";
|
|
490
|
-
import { Tool, Toolkit } from "effect/
|
|
534
|
+
import { Tool, Toolkit } from "effect/ai";
|
|
491
535
|
|
|
492
536
|
const Ping = Tool.make("ping", { description: "Liveness check.", parameters: Tool.EmptyParams });
|
|
493
537
|
const Tools = Toolkit.make(Ping);
|
package/ToolFailure.js
CHANGED
|
@@ -9,7 +9,7 @@ const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
|
|
|
9
9
|
* @remarks
|
|
10
10
|
* Core sends a declared failure that is an `Error` instance — every
|
|
11
11
|
* `Schema.TaggedError` is — as `isError: true` with `error.message` as the only
|
|
12
|
-
* text and no `structuredContent` (effect `
|
|
12
|
+
* text and no `structuredContent` (effect `ai/McpServer.ts`,
|
|
13
13
|
* `declaredFailureResult`). So the remediation must be folded into `message`
|
|
14
14
|
* when the error is constructed: whatever is not in the message never reaches
|
|
15
15
|
* the agent.
|
|
@@ -26,6 +26,9 @@ const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
|
|
|
26
26
|
*
|
|
27
27
|
* @example
|
|
28
28
|
* ```ts
|
|
29
|
+
* import { ToolFailure } from "@effected/mcp";
|
|
30
|
+
* import { Schema } from "effect";
|
|
31
|
+
*
|
|
29
32
|
* class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
|
|
30
33
|
* const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
|
|
31
34
|
* new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
|
package/ToolInputSchema.js
CHANGED
|
@@ -77,11 +77,15 @@ const collect = (payload, root) => {
|
|
|
77
77
|
if (match === void 0) extra.push(key);
|
|
78
78
|
else patterned.push([key, match[1]]);
|
|
79
79
|
}
|
|
80
|
-
if (extra.length > 0 && isClosed)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
if (extra.length > 0 && isClosed) {
|
|
81
|
+
const acceptedPatterns = patterns.map(([pattern]) => pattern);
|
|
82
|
+
out.push({
|
|
83
|
+
path,
|
|
84
|
+
unknown: extra,
|
|
85
|
+
accepted,
|
|
86
|
+
...acceptedPatterns.length > 0 ? { acceptedPatterns } : {}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
85
89
|
for (const key of accepted) if (Object.hasOwn(value, key)) walk(value[key], properties[key], [...path, key], depth + 1);
|
|
86
90
|
for (const [key, schema] of patterned) walk(value[key], schema, [...path, key], depth + 1);
|
|
87
91
|
if (additional !== void 0) for (const key of extra) walk(value[key], additional, [...path, key], depth + 1);
|
|
@@ -94,10 +98,12 @@ const collect = (payload, root) => {
|
|
|
94
98
|
* depth, a message naming them all, and an object-rooted form of a union.
|
|
95
99
|
*
|
|
96
100
|
* @remarks
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
+
* For a `Tool.make` tool, core's strict decode already names every unknown
|
|
102
|
+
* key, with every missing or invalid field, in one response. Core never
|
|
103
|
+
* validates a `Tool.dynamic` tool's raw JSON Schema, so these walkers are
|
|
104
|
+
* for that case: a handler runs them against the schema it registered.
|
|
105
|
+
* They run over the schema as served, so what they accept matches what the
|
|
106
|
+
* tool advertises:
|
|
101
107
|
*
|
|
102
108
|
* - Only `additionalProperties: false` closes a node, as in JSON Schema; core
|
|
103
109
|
* emits it on every object node of a strict tool and `true` otherwise.
|
|
@@ -108,6 +114,9 @@ const collect = (payload, root) => {
|
|
|
108
114
|
* - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
|
|
109
115
|
* honoured), and `patternProperties` are followed; a key matched by a
|
|
110
116
|
* pattern is accepted and walked with the first matching pattern's schema.
|
|
117
|
+
* Effect serves a pattern-keyed `Schema.Record` with `patternProperties`
|
|
118
|
+
* only when the key pattern's RegExp has the `u` flag; without it the
|
|
119
|
+
* record is served open, and every key is accepted.
|
|
111
120
|
* - The payload is untrusted: the walk stops descending at depth 256 rather
|
|
112
121
|
* than overflowing the stack, so a deeper unknown key goes unreported and
|
|
113
122
|
* is left for decoding. Every hop counts toward that cap: each property or
|
|
@@ -121,17 +130,22 @@ var ToolInputSchema = class {
|
|
|
121
130
|
/** Every level of `payload` that carries a key `schema` does not accept. */
|
|
122
131
|
static unknownKeys = (payload, schema) => collect(payload, schema);
|
|
123
132
|
/**
|
|
124
|
-
* `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level
|
|
125
|
-
*
|
|
126
|
-
*
|
|
133
|
+
* `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level,
|
|
134
|
+
* followed by `Accepted keys matching: ^x-.` when the level also accepts
|
|
135
|
+
* keys by pattern. A level that accepts keys only by pattern names its
|
|
136
|
+
* patterns alone, never `Accepted params: (none).` Each echoed key and
|
|
137
|
+
* pattern is truncated, and at most 20 keys, 20 patterns and 20 levels are
|
|
138
|
+
* named; a trailing `(and N more levels).` counts the levels left out.
|
|
127
139
|
*/
|
|
128
140
|
static formatUnknownKeys = (levels, options = {}) => {
|
|
129
141
|
const limit = options.echoLimit ?? ToolFailure.ECHO_LIMIT;
|
|
130
142
|
const sentences = levels.slice(0, MAX_ECHOED).map((level) => {
|
|
131
143
|
const shown = level.unknown.slice(0, MAX_ECHOED).map((key) => ToolFailure.truncate([...level.path, key].join("."), limit));
|
|
132
144
|
const more = level.unknown.length > MAX_ECHOED ? ` (and ${level.unknown.length - MAX_ECHOED} more)` : "";
|
|
133
|
-
const
|
|
134
|
-
|
|
145
|
+
const patterns = (level.acceptedPatterns ?? []).slice(0, MAX_ECHOED).map((pattern) => ToolFailure.truncate(pattern, limit));
|
|
146
|
+
const params = level.accepted.length > 0 ? ` Accepted params: ${level.accepted.join(", ")}.` : patterns.length === 0 ? " Accepted params: (none)." : "";
|
|
147
|
+
const matching = patterns.length > 0 ? ` Accepted keys matching: ${patterns.join(", ")}.` : "";
|
|
148
|
+
return `Unrecognized parameter(s): ${shown.join(", ")}${more}.${params}${matching}`;
|
|
135
149
|
});
|
|
136
150
|
const hidden = levels.length > MAX_ECHOED ? ` (and ${levels.length - MAX_ECHOED} more levels).` : "";
|
|
137
151
|
return `${sentences.join(" ")}${hidden}`;
|
package/ToolRefusal.js
CHANGED
|
@@ -14,11 +14,14 @@ import { Schema } from "effect";
|
|
|
14
14
|
* {@link ToolRefusal.refuse}; its message already carries the remediation,
|
|
15
15
|
* because a declared `Error` failure sends `message` and nothing else.
|
|
16
16
|
*
|
|
17
|
-
* Audit every throw path
|
|
18
|
-
*
|
|
17
|
+
* Audit every throw path of a tool: any failure left undeclared loses its
|
|
18
|
+
* message on the wire, however carefully it was worded.
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
|
22
|
+
* import { ToolFailure, ToolRefusal } from "@effected/mcp";
|
|
23
|
+
* import { Tool } from "effect/ai";
|
|
24
|
+
*
|
|
22
25
|
* const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
|
|
23
26
|
* // in the handler:
|
|
24
27
|
* return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
|