@vitest-agent/mcp 4.3.2 → 5.0.1

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/tools/tdd-task.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { IdempotentReplayMarker } from "../utils/replay-marker.js";
2
+ import { McpToolkit, ToolFailure, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
2
3
  import { Effect, Match, Option, Schema } from "effect";
3
4
  import { DataReader, DataStore } from "@vitest-agent/engine";
4
5
  import { Tool } from "effect/unstable/ai";
@@ -82,14 +83,14 @@ const TddTaskResumeMissing = Schema.Struct({
82
83
  found: Schema.Literal(false),
83
84
  tddTaskId: Schema.Number
84
85
  }).annotate({ identifier: "TddTaskResumeMissing" });
85
- const TddTaskResult = Schema.Union([
86
+ const TddTaskResult = ToolOutputSchema.objectRooted(Schema.Union([
86
87
  TddTaskStartOk,
87
88
  TddTaskEndOk,
88
89
  TddTaskGetFound,
89
90
  TddTaskGetMissing,
90
91
  TddTaskResumeFound,
91
92
  TddTaskResumeMissing
92
- ]).annotate({
93
+ ])).annotate({
93
94
  identifier: "TddTaskResult",
94
95
  title: "tdd_task result",
95
96
  description: "Discriminate on `action`. `get` and `resume` further discriminate on `found`. `get` carries the full nested task tree."
@@ -134,8 +135,9 @@ const TddTaskInput = Schema.Union([
134
135
  ]);
135
136
  /**
136
137
  * Handler for {@link tddTaskTool}. A `start` with neither `sessionId` nor
137
- * `chatId`, an unknown `chatId`, or a blank `runId` fails as a defect (the
138
- * `UnexpectedToolError` envelope on the wire), as the retired tRPC procedure did.
138
+ * `chatId`, an unknown `sessionId` or `chatId`, or a blank `runId` fails with a
139
+ * {@link ToolRefusal} (an `isError` result naming the fix). A store failure is
140
+ * a defect.
139
141
  *
140
142
  * @public
141
143
  */
@@ -144,13 +146,23 @@ const handleTddTask = (input) => Match.value(input).pipe(Match.discriminatorsExh
144
146
  const reader = yield* DataReader;
145
147
  const store = yield* DataStore;
146
148
  let sessionId;
147
- if (variant.sessionId !== void 0) sessionId = variant.sessionId;
148
- else if (variant.chatId !== void 0) {
149
+ if (variant.sessionId !== void 0) {
150
+ if (Option.isNone(yield* reader.getSessionById(variant.sessionId))) return yield* Effect.fail(ToolRefusal.refuse(`Unknown sessionId ${variant.sessionId}.`, { hint: "Pass chatId (the host chat UUID) instead, or a sessionId that exists." }));
151
+ sessionId = variant.sessionId;
152
+ } else if (variant.chatId !== void 0) {
149
153
  const opt = yield* reader.getSessionByChatId(variant.chatId);
150
- if (Option.isNone(opt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${variant.chatId}. Run record session-start first.`));
154
+ if (Option.isNone(opt)) return yield* Effect.fail(ToolRefusal.refuse(`Unknown chatId ${ToolFailure.truncate(variant.chatId)}: no session is recorded for it.`, { hint: "The plugin's SessionStart hook records the session (vitest-agent agent record session-start); check that it ran for this chat, or pass a sessionId that exists." }));
151
155
  sessionId = opt.value.id;
152
- } else return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: provide sessionId or chatId"));
153
- if (variant.runId !== void 0 && variant.runId.trim().length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: runId must not be blank"));
156
+ } else return yield* Effect.fail(ToolRefusal.refuse("tdd_task action='start' needs a session.", {
157
+ hint: "Pass sessionId (sessions.id) or chatId (the host chat UUID).",
158
+ suggestedTool: "tdd_task",
159
+ suggestedArgs: { action: "start" }
160
+ }));
161
+ if (variant.runId !== void 0 && variant.runId.trim().length === 0) return yield* Effect.fail(ToolRefusal.refuse("tdd_task action='start': runId must not be blank.", {
162
+ hint: "Omit runId, or pass a non-empty dispatch id.",
163
+ suggestedTool: "tdd_task",
164
+ suggestedArgs: { action: "start" }
165
+ }));
154
166
  return {
155
167
  action: "start",
156
168
  tddTaskId: yield* store.writeTddTask({
@@ -234,18 +246,18 @@ const handleTddTask = (input) => Match.value(input).pipe(Match.discriminatorsExh
234
246
  artifactsRecorded: tdd.artifacts.length
235
247
  };
236
248
  })
237
- })).pipe(Effect.orDie);
249
+ })).pipe(Effect.catchTag("DataStoreError", Effect.die));
238
250
  /**
239
251
  * The Effect-native `tdd_task` tool.
240
252
  *
241
253
  * @public
242
254
  */
243
- const tddTaskTool = Tool.make("tdd_task", {
255
+ const tddTaskTool = McpToolkit.unionTool("tdd_task", {
244
256
  description: "Use to manage a TDD task lifecycle, with an action discriminator: action='start' (goal, sessionId|chatId, parentTddTaskId?, startedAt?, runId?) opens a new task; action='end' (tddTaskId, outcome, summaryNoteId?) closes one; action='get' (tddTaskId) returns the full task detail; action='resume' (tddTaskId) returns a compact digest.",
245
257
  parameters: TddTaskInput,
246
258
  success: TddTaskResult,
247
- dependencies: [DataReader, DataStore]
248
- }).annotate(Tool.Title, "TDD task").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
259
+ failure: ToolRefusal
260
+ }).addDependency(DataReader).addDependency(DataStore).annotate(Tool.Title, "TDD task").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
249
261
 
250
262
  //#endregion
251
263
  export { TddTaskInput, TddTaskResult, handleTddTask, tddTaskTool };
package/tools/test.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
2
+ import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
2
3
  import { Effect, Match, Option, Schema } from "effect";
3
4
  import { DataReader } from "@vitest-agent/engine";
4
5
  import { Tool } from "effect/unstable/ai";
@@ -149,7 +150,7 @@ const TestArtifactsResult = Schema.Struct({
149
150
  *
150
151
  * @public
151
152
  */
152
- const TestResult = Schema.Union([
153
+ const TestResult = ToolOutputSchema.objectRooted(Schema.Union([
153
154
  TestListResult,
154
155
  TestGetFound,
155
156
  TestGetMissing,
@@ -157,7 +158,7 @@ const TestResult = Schema.Union([
157
158
  TestForTagResult,
158
159
  TestAnnotationsResult,
159
160
  TestArtifactsResult
160
- ]).annotate({
161
+ ])).annotate({
161
162
  identifier: "TestResult",
162
163
  title: "test result",
163
164
  description: "Discriminate on `action`. `get` further discriminates on `found`. `list`, `for_file`, `for_tag`, `annotations`, and `artifacts` all carry counted arrays — `list` and `for_tag` group by project; `annotations` and `artifacts` are scoped to one test and return attachment descriptors — an inline `body` comes back only when `maxBytes` is passed and the running total stays inside it."
@@ -335,12 +336,12 @@ const handleTest = (input) => Match.value(input).pipe(Match.discriminatorsExhaus
335
336
  *
336
337
  * @public
337
338
  */
338
- const testTool = Tool.make("test", {
339
+ const testTool = McpToolkit.unionTool("test", {
339
340
  description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project; action='annotations' (fullName, project?, modulePath?) returns the test annotations the author recorded via context.annotate; action='artifacts' (fullName, project?, modulePath?) returns the test artifacts recorded for the test — both return attachment descriptors (contentType, path, byteSize) and omit inline bodies unless maxBytes (a non-negative integer byte budget for the whole response, default 0) is passed, and neither has anything to do with TDD artifacts (see tdd_artifact_list). structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
340
341
  parameters: TestInput,
341
342
  success: TestResult,
342
- dependencies: [DataReader]
343
- }).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
343
+ failure: ToolRefusal
344
+ }).addDependency(DataReader).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
344
345
 
345
346
  //#endregion
346
347
  export { TestInput, TestResult, handleTest, testTool };
package/tools/trends.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ToolOutputSchema } from "@effected/mcp";
1
2
  import { Effect, Option, Schema } from "effect";
2
3
  import { DataReader } from "@vitest-agent/engine";
3
4
  import { Tool } from "effect/unstable/ai";
@@ -18,7 +19,7 @@ const TrendsAbsent = Schema.Struct({
18
19
  *
19
20
  * @public
20
21
  */
21
- const TestTrendsResult = Schema.Union([TrendsAvailable, TrendsAbsent]).annotate({
22
+ const TestTrendsResult = ToolOutputSchema.objectRooted(Schema.Union([TrendsAvailable, TrendsAbsent])).annotate({
22
23
  identifier: "TestTrendsResult",
23
24
  title: "test_trends result",
24
25
  description: "Coverage trend record per project. Discriminate on `dataAvailable` to handle the cold-start case."
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.2"
8
+ "packageVersion": "7.59.1"
9
9
  }
10
10
  ]
11
11
  }
package/version.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @public
9
9
  */
10
- const CURRENT_MCP_VERSION = "4.3.2";
10
+ const CURRENT_MCP_VERSION = "5.0.1";
11
11
 
12
12
  //#endregion
13
13
  export { CURRENT_MCP_VERSION };
@@ -1,328 +0,0 @@
1
- import { buildUnexpectedToolErrorEnvelope } from "./utils/tool-error-envelope.js";
2
- import { Cause, Context, Effect, ErrorReporter, Layer, Option, References, Result, Schema, Stream } from "effect";
3
- import { AiError, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
4
- import { HttpServerRequest } from "effect/unstable/http";
5
-
6
- //#region src/register-toolkit.ts
7
- const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
8
- /** Top-level discriminants a union of parameter shapes may dispatch on. */
9
- const DISCRIMINANT_KEYS = ["action", "kind"];
10
- /** Length cap for every echoed unknown key, so a hostile payload cannot balloon the error. */
11
- const ECHO_LIMIT = 200;
12
- const truncate = (value) => value.length > ECHO_LIMIT ? `${value.slice(0, ECHO_LIMIT)}…` : value;
13
- const literalValues = (node) => {
14
- if (!isPlainObject(node)) return void 0;
15
- if (Array.isArray(node.enum)) return node.enum;
16
- if ("const" in node) return [node.const];
17
- };
18
- /**
19
- * The discriminant every member of a union shares as a literal-valued
20
- * property, or `undefined` when the members do not form such a union.
21
- */
22
- const findDiscriminant = (members) => {
23
- if (members.length === 0) return void 0;
24
- for (const key of DISCRIMINANT_KEYS) if (members.every((member) => {
25
- if (!isPlainObject(member) || !isPlainObject(member.properties)) return false;
26
- return literalValues(member.properties[key]) !== void 0;
27
- })) return key;
28
- };
29
- const REF_PREFIX = "#/$defs/";
30
- const lookupRef = (ref, root) => {
31
- if (typeof ref !== "string" || !ref.startsWith(REF_PREFIX)) return void 0;
32
- const defs = root.$defs;
33
- if (!isPlainObject(defs)) return void 0;
34
- const target = defs[ref.slice(8)];
35
- return isPlainObject(target) ? target : void 0;
36
- };
37
- /**
38
- * Replace a `$ref` node with its `$defs` target merged under the node's
39
- * own keywords (the node's keys win). Follows chained refs; gives up on a
40
- * cycle or a dangling ref and returns the node as-is.
41
- */
42
- const inlineRef = (node, root, seen = /* @__PURE__ */ new Set()) => {
43
- const target = lookupRef(node.$ref, root);
44
- if (target === void 0 || seen.has(node.$ref)) return node;
45
- const { $ref, ...rest } = node;
46
- return inlineRef({
47
- ...target,
48
- ...rest
49
- }, root, /* @__PURE__ */ new Set([...seen, $ref]));
50
- };
51
- /**
52
- * A schema whose root (or whose top-level union members) is a `$ref` —
53
- * what Effect emits for any schema carrying an `identifier` annotation —
54
- * is inlined so the object / discriminant checks below see the real
55
- * shape. `$defs` is kept for any nested references.
56
- *
57
- * @internal
58
- */
59
- const inlineRootRefs = (schema) => {
60
- const root = inlineRef(schema, schema);
61
- const combinator = Array.isArray(root.anyOf) ? "anyOf" : Array.isArray(root.oneOf) ? "oneOf" : void 0;
62
- if (combinator === void 0) return root;
63
- const members = root[combinator].map((member) => isPlainObject(member) ? inlineRef(member, schema) : member);
64
- return {
65
- ...root,
66
- [combinator]: members
67
- };
68
- };
69
- /**
70
- * Make an input JSON Schema servable as an MCP tool input: inline a `$ref`
71
- * root (and `$ref` members of a top-level union), and rewrite a top-level
72
- * union of discriminated object shapes to `{ type: "object", oneOf,
73
- * "x-discriminator" }` so it satisfies MCP's object-root requirement. The
74
- * object nodes themselves are left as Effect emitted them — closed, when the
75
- * document was built with `onExcessProperty: "error"`.
76
- *
77
- * @internal
78
- */
79
- const objectRootedInputSchema = (schema) => {
80
- const root = inlineRootRefs(schema);
81
- const members = root.anyOf ?? root.oneOf;
82
- if (Array.isArray(members) && root.type === void 0) {
83
- const discriminant = findDiscriminant(members);
84
- if (discriminant !== void 0) {
85
- const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = root;
86
- return {
87
- type: "object",
88
- ...rest,
89
- oneOf: members,
90
- "x-discriminator": discriminant
91
- };
92
- }
93
- }
94
- return root;
95
- };
96
- const resolveRef = (node, root) => lookupRef(node.$ref, root) ?? node;
97
- /** Pick the union member a payload object matches, or `undefined` when none does unambiguously. */
98
- const selectMember = (members, value, root) => {
99
- const resolved = members.map((m) => isPlainObject(m) ? resolveRef(m, root) : m);
100
- const discriminant = findDiscriminant(resolved);
101
- if (discriminant !== void 0) {
102
- const actual = value[discriminant];
103
- return resolved.find((member) => {
104
- if (!isPlainObject(member) || !isPlainObject(member.properties)) return false;
105
- return literalValues(member.properties[discriminant])?.includes(actual) ?? false;
106
- });
107
- }
108
- const objectMembers = resolved.filter((m) => isPlainObject(m) && isPlainObject(m.properties));
109
- return objectMembers.length === 1 ? objectMembers[0] : void 0;
110
- };
111
- /**
112
- * Walk `value` against `schema`, collecting — at every object level — the
113
- * keys the payload carries that the schema does not declare.
114
- *
115
- * @internal
116
- */
117
- const collectUnknownKeys = (value, schema) => {
118
- const out = [];
119
- const walk = (current, rawNode, path) => {
120
- if (!isPlainObject(rawNode)) return;
121
- const node = resolveRef(rawNode, schema);
122
- if (Array.isArray(node.allOf)) {
123
- const merged = {};
124
- for (const member of node.allOf) {
125
- const resolvedMember = isPlainObject(member) ? resolveRef(member, schema) : member;
126
- if (isPlainObject(resolvedMember) && isPlainObject(resolvedMember.properties)) Object.assign(merged, resolvedMember.properties);
127
- }
128
- if (Object.keys(merged).length > 0) {
129
- walk(current, {
130
- ...node,
131
- allOf: void 0,
132
- properties: {
133
- ...merged,
134
- ...isPlainObject(node.properties) ? node.properties : {}
135
- }
136
- }, path);
137
- return;
138
- }
139
- }
140
- if (isPlainObject(node.properties)) {
141
- if (!isPlainObject(current)) return;
142
- const accepted = Object.keys(node.properties);
143
- const unknown = Object.keys(current).filter((key) => !accepted.includes(key));
144
- if (unknown.length > 0 && node.additionalProperties !== true && !isPlainObject(node.additionalProperties)) out.push({
145
- path,
146
- unknown,
147
- accepted
148
- });
149
- for (const key of accepted) if (key in current) walk(current[key], node.properties[key], [...path, key]);
150
- if (isPlainObject(node.additionalProperties)) for (const key of unknown) walk(current[key], node.additionalProperties, [...path, key]);
151
- return;
152
- }
153
- const members = node.oneOf ?? node.anyOf;
154
- if (Array.isArray(members)) {
155
- if (!isPlainObject(current)) return;
156
- const member = selectMember(members, current, schema);
157
- if (member !== void 0) walk(current, member, path);
158
- return;
159
- }
160
- if (Array.isArray(current)) {
161
- if (Array.isArray(node.prefixItems)) for (const [index, item] of node.prefixItems.entries()) walk(current[index], item, [...path, String(index)]);
162
- if (isPlainObject(node.items)) for (const [index, item] of current.entries()) walk(item, node.items, [...path, String(index)]);
163
- return;
164
- }
165
- if (isPlainObject(current) && node.type === "object" && node.additionalProperties === false) {
166
- const unknown = Object.keys(current);
167
- if (unknown.length > 0) out.push({
168
- path,
169
- unknown,
170
- accepted: []
171
- });
172
- }
173
- };
174
- walk(value, schema, []);
175
- return out;
176
- };
177
- /**
178
- * The exact wording the pre-Effect `strict()` helper produced, so agents
179
- * that learned to self-correct on it keep working.
180
- */
181
- const formatUnknownKeys = (levels) => levels.map((level) => {
182
- const qualify = (key) => truncate([...level.path, key].join("."));
183
- const accepted = level.accepted.length === 0 ? "(none)" : level.accepted.join(", ");
184
- return `Unrecognized parameter(s): ${level.unknown.map(qualify).join(", ")}. Accepted params: ${accepted}`;
185
- }).join(" ");
186
- /**
187
- * MCP models `structuredContent` as a JSON object, so a `null`, array or
188
- * primitive encoded result is omitted rather than sent through as-is.
189
- */
190
- const toStructuredContent = (value) => isPlainObject(value) ? value : void 0;
191
- const toolErrorResult = (message) => new McpSchema.CallToolResult({
192
- isError: true,
193
- content: [{
194
- type: "text",
195
- text: message
196
- }]
197
- });
198
- const toolResultContent = (encoded) => encoded === void 0 ? [] : [{
199
- type: "text",
200
- text: JSON.stringify(encoded)
201
- }];
202
- const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
203
- const isParameterValidationError = (error) => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError";
204
- /**
205
- * The input JSON Schema a tool is served with: the document rc.116's
206
- * `toolInputJsonSchema` builds for a strict tool (`Schema.toJsonSchemaDocument`
207
- * with `onExcessProperty: "error"`, so every object node carries
208
- * `additionalProperties: false`; definitions attached as `$defs`), made
209
- * object-rooted by {@link objectRootedInputSchema}.
210
- *
211
- * @internal
212
- */
213
- const servedInputJsonSchema = (schema) => {
214
- const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" });
215
- return objectRootedInputSchema(Object.keys(document.definitions).length === 0 ? document.schema : {
216
- ...document.schema,
217
- $defs: document.definitions
218
- });
219
- };
220
- const envelopeResult = (toolName, err) => {
221
- const envelope = buildUnexpectedToolErrorEnvelope(toolName, err);
222
- return new McpSchema.CallToolResult({
223
- isError: true,
224
- structuredContent: envelope,
225
- content: [{
226
- type: "text",
227
- text: JSON.stringify(envelope)
228
- }]
229
- });
230
- };
231
- /**
232
- * Register every tool of `toolkit` with the ambient `McpServer` under the
233
- * strict contract described in the module docs. A port of rc.116's
234
- * `McpServer.registerToolkit`; the deviations are enumerated in the module
235
- * header.
236
- *
237
- * @internal
238
- */
239
- const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
240
- const registry = yield* McpServer.McpServer;
241
- const built = yield* toolkit.pipe(Effect.updateContext((context) => {
242
- const services = new Map(context.mapUnsafe);
243
- for (const tool of Object.values(toolkit.tools)) {
244
- const handler = services.get(tool.id);
245
- if (handler !== void 0) services.set(tool.id, {
246
- ...handler,
247
- context: omitRequestServices(handler.context)
248
- });
249
- }
250
- return Context.makeUnsafe(services);
251
- }));
252
- const services = omitRequestServices(yield* Effect.context());
253
- const reportCause = (cause) => Effect.provideContext(ErrorReporter.report(cause), services);
254
- const registrations = [];
255
- for (const tool of Object.values(built.tools)) {
256
- const internalToolError = (cause) => {
257
- const failure = Cause.findFail(cause);
258
- return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(`tool ${tool.name} failed`, cause).pipe(Effect.andThen(reportCause(cause)), Effect.as(envelopeResult(tool.name, Result.isSuccess(failure) ? failure.success.error : Cause.squash(cause))));
259
- };
260
- if (Tool.isDynamic(tool)) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
261
- const decodeOptions = { onExcessProperty: "error" };
262
- const annotations = tool.annotations;
263
- const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
264
- const isDeclaredFailure = Schema.is(tool.failureSchema);
265
- const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema);
266
- const declaredFailureResult = (error) => error instanceof Error ? Effect.succeed(toolErrorResult(error.message)) : Effect.map(encodeFailure(error), (encoded) => new McpSchema.CallToolResult({
267
- isError: true,
268
- content: toolResultContent(encoded)
269
- }));
270
- const handleCause = (cause) => {
271
- const failure = Cause.findFail(cause);
272
- if (Result.isSuccess(failure)) {
273
- const error = failure.success.error;
274
- const origin = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin);
275
- if (origin === "parameters" && isParameterValidationError(error)) return Effect.fail(new McpSchema.InvalidParams({ message: error.reason.message }));
276
- if (origin === "handler" && isDeclaredFailure(error)) return Effect.catchCause(declaredFailureResult(error), internalToolError);
277
- }
278
- return internalToolError(cause);
279
- };
280
- const outputJsonSchema = inlineRootRefs(Tool.getJsonSchemaFromSchema(tool.successSchema));
281
- const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(outputJsonSchema).pipe(Effect.orDie) : void 0;
282
- const servedInput = servedInputJsonSchema(tool.parametersSchema);
283
- const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(servedInput).pipe(Effect.orDie);
284
- const description = Tool.getDescription(tool);
285
- const mcpTool = new McpSchema.Tool({
286
- name: tool.name,
287
- ...description === void 0 ? {} : { description },
288
- inputSchema,
289
- ...outputSchema === void 0 ? {} : { outputSchema },
290
- annotations: {
291
- ...Context.getOption(annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
292
- readOnlyHint: Context.get(annotations, Tool.Readonly),
293
- destructiveHint: Context.get(annotations, Tool.Destructive),
294
- idempotentHint: Context.get(annotations, Tool.Idempotent),
295
- openWorldHint: Context.get(annotations, Tool.OpenWorld)
296
- },
297
- ...toolMeta === void 0 ? {} : { _meta: toolMeta }
298
- });
299
- registrations.push({
300
- tool: mcpTool,
301
- annotations,
302
- handle(payload) {
303
- const raw = payload ?? {};
304
- const unknownKeys = collectUnknownKeys(raw, servedInput);
305
- if (unknownKeys.length > 0) return Effect.fail(new McpSchema.InvalidParams({ message: formatUnknownKeys(unknownKeys) }));
306
- return built.handle(tool.name, raw, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => result.isFailure && result.failureOrigin !== "handler" ? Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result"))) : Effect.succeed(new McpSchema.CallToolResult({
307
- isError: result.isFailure,
308
- structuredContent: result.isFailure ? void 0 : toStructuredContent(result.encodedResult),
309
- content: toolResultContent(result.encodedResult)
310
- }))), Effect.catchCause(handleCause), Effect.provideContext(services));
311
- }
312
- });
313
- }
314
- for (const registration of registrations) yield* registry.addTool(registration);
315
- });
316
- /**
317
- * A layer that registers `toolkit` with the strict contract. Same shape
318
- * as `McpServer.toolkit`: it requires the toolkit's handlers (from
319
- * `kit.toLayer(handlers)`) and every service the tools declare in
320
- * `dependencies`, and shares the memoized `McpServer` with the transport
321
- * layer it is provided into.
322
- *
323
- * @public
324
- */
325
- const registerStrictToolkit = (toolkit) => Layer.effectDiscard(registerStrictToolkitEffect(toolkit)).pipe(Layer.provide(McpServer.McpServer.layer));
326
-
327
- //#endregion
328
- export { collectUnknownKeys, inlineRootRefs, objectRootedInputSchema, registerStrictToolkit, registerStrictToolkitEffect, servedInputJsonSchema };
@@ -1,13 +0,0 @@
1
- //#region src/utils/crash-guards.ts
2
- /**
3
- * @param transportConnected - whether `server.connect(transport)` has
4
- * already resolved for this process
5
- * @returns `true` when the process should exit rather than continue
6
- * @public
7
- */
8
- function shouldExitOnUncaughtException(transportConnected) {
9
- return !transportConnected;
10
- }
11
-
12
- //#endregion
13
- export { shouldExitOnUncaughtException };
@@ -1,61 +0,0 @@
1
- //#region src/utils/tool-error-envelope.ts
2
- /**
3
- * Fixed fallback used when even *introspecting* the thrown value throws.
4
- * Deliberately a constant — anything derived from `err` could throw again
5
- * on the recovery path.
6
- */
7
- const UNREADABLE_THROWN_VALUE = "<unreadable thrown value>";
8
- /**
9
- * Coerces an unknown thrown value into a display-safe message without
10
- * risking a second throw (a getter-backed `.message` can itself throw —
11
- * see `@vitest-agent/sdk`'s `coerceErrorField` for the same concern on
12
- * raw Vitest error objects).
13
- *
14
- * The outer guard matters as much as the inner one: `err instanceof
15
- * Error` walks the prototype chain, which a `Proxy` `getPrototypeOf`
16
- * trap can hijack and throw from, and `String(err)` invokes
17
- * `Symbol.toPrimitive` / `toString`, which a trap can hijack too. A
18
- * throw here escapes the envelope builder entirely and the agent gets
19
- * nothing structured back — the exact degradation this module exists to
20
- * prevent (issue #243).
21
- */
22
- function coerceThrownMessage(err) {
23
- try {
24
- if (typeof err === "string") return err;
25
- if (err instanceof Error) try {
26
- const message = err.message;
27
- return typeof message === "string" ? message : String(message);
28
- } catch {
29
- return "<unreadable Error.message>";
30
- }
31
- return String(err);
32
- } catch {
33
- return UNREADABLE_THROWN_VALUE;
34
- }
35
- }
36
- /**
37
- * Builds the structured envelope a tool's catch-all wrapper returns
38
- * when its handler throws unexpectedly.
39
- *
40
- * @param toolName - the MCP tool name under which the handler was registered
41
- * @param err - the value thrown or the rejection reason
42
- * @public
43
- */
44
- function buildUnexpectedToolErrorEnvelope(toolName, err) {
45
- return {
46
- ok: false,
47
- error: {
48
- _tag: "UnexpectedToolError",
49
- tool: toolName,
50
- message: coerceThrownMessage(err),
51
- remediation: {
52
- suggestedTool: toolName,
53
- suggestedArgs: {},
54
- humanHint: `The "${toolName}" tool's handler threw before producing a result (unrelated to your input in most cases). Retry the call; if it persists, check the MCP server's stderr for the logged error.`
55
- }
56
- }
57
- };
58
- }
59
-
60
- //#endregion
61
- export { buildUnexpectedToolErrorEnvelope };