Skip to content

Commit 3a76b1f

Browse files
committed
Verify connected tool provenance over MCP
1 parent 4d07ece commit 3a76b1f

1 file changed

Lines changed: 59 additions & 2 deletions

File tree

‎e2e/scenarios/tool-call-contract.test.ts‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { randomBytes } from "node:crypto";
2121
import { createServer } from "node:http";
2222

2323
import { expect } from "@effect/vitest";
24-
import { Effect } from "effect";
24+
import { Effect, Schema } from "effect";
2525
import { composePluginApi } from "@executor-js/api/server";
2626
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
2727
import {
@@ -38,6 +38,13 @@ import type { McpSession } from "../src/surfaces/mcp";
3838

3939
const api = composePluginApi([openApiHttpPlugin()] as const);
4040

41+
const completion = Schema.Struct({
42+
structuredContent: Schema.Struct({
43+
status: Schema.String,
44+
toolName: Schema.optional(Schema.String),
45+
}),
46+
});
47+
4148
const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;
4249

4350
type UpstreamHandle = {
@@ -136,6 +143,13 @@ const widgetsSpec = (baseUrl: string): string =>
136143
info: { title: "Widgets API", version: "1.0.0" },
137144
servers: [{ url: baseUrl }],
138145
paths: {
146+
"/widgets/count": {
147+
get: {
148+
operationId: "countWidgets",
149+
summary: "Count widgets",
150+
responses: { "200": { description: "widget count" } },
151+
},
152+
},
139153
"/widgets": {
140154
get: {
141155
operationId: "listWidgets",
@@ -218,6 +232,47 @@ scenario(
218232
"toolPaths",
219233
);
220234
expect(upstream.requests(), "the upstream served exactly one call").toBe(1);
235+
const discovery = yield* executeApproved(
236+
session,
237+
'return await tools.search({ query: "widgets" });',
238+
);
239+
const discoveryResult = yield* Schema.decodeUnknownEffect(completion)(discovery.raw);
240+
expect(discoveryResult.structuredContent.toolName).toBeUndefined();
241+
expect(discovery.raw).not.toHaveProperty("structuredContent.toolPaths");
242+
243+
const failed = yield* executeApproved(
244+
session,
245+
invokeByAddressCode(address!.replace(/listWidgets$/, "missingWidget"), {}),
246+
);
247+
const failedResult = yield* Schema.decodeUnknownEffect(completion)(failed.raw);
248+
expect(failedResult.structuredContent.toolName).toBeUndefined();
249+
expect(failed.raw).not.toHaveProperty("structuredContent.toolPaths");
250+
251+
const repeated = yield* executeApproved(
252+
session,
253+
`
254+
await tools[${JSON.stringify(path)}]({});
255+
return await tools[${JSON.stringify(path)}]({});
256+
`,
257+
);
258+
const repeatedResult = yield* Schema.decodeUnknownEffect(completion)(repeated.raw);
259+
expect(repeatedResult.structuredContent.toolName).toBe(path);
260+
expect(repeated.raw).not.toHaveProperty("structuredContent.toolPaths");
261+
const anotherAddress = tools
262+
.filter((tool) => String(tool.integration) === slug)
263+
.map((tool) => String(tool.address))
264+
.find((candidate) => candidate.endsWith("countWidgets"));
265+
const anotherPath = yield* Schema.decodeUnknownEffect(Schema.String)(anotherAddress);
266+
const multiple = yield* executeApproved(
267+
session,
268+
`
269+
await tools[${JSON.stringify(path)}]({});
270+
return await tools[${JSON.stringify(anotherPath.replace(/^tools\./, ""))}]({});
271+
`,
272+
);
273+
const multipleResult = yield* Schema.decodeUnknownEffect(completion)(multiple.raw);
274+
expect(multipleResult.structuredContent.toolName).toBeUndefined();
275+
expect(multiple.raw).not.toHaveProperty("structuredContent.toolPaths");
221276

222277
// 2a. A wrong TOOL name on a live connection: tool_not_found, and the
223278
// suggestions name the connection's real tools so the agent can
@@ -267,7 +322,9 @@ scenario(
267322
"the defect mask never surfaces for a missing connection",
268323
).not.toContain("Internal tool error");
269324

270-
expect(upstream.requests(), "no misaddressed call ever reached the upstream").toBe(1);
325+
expect(upstream.requests(), "only the five successful calls reached the upstream").toBe(
326+
5,
327+
);
271328
}),
272329
// Selfhost shares one workspace identity — leaked resources fail other
273330
// scenarios' zero-state assertions.

0 commit comments

Comments
 (0)