Files
shockbot/mcp/toolFiltering.test.ts
T
Colin McDonnell e2d8dfeebf Clarify shell guidance and delegation checklists
Standardize orchestrator/subagent instructions on the MCP shell tool and format mode guidance as explicit checklists to make delegation flows easier to follow. Bump the action package version to 0.0.171 for this release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 01:28:50 +00:00

152 lines
5.9 KiB
TypeScript

import { createServer } from "node:net";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { type } from "arktype";
import { FastMCP } from "fastmcp";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { execute, tool } from "./shared.ts";
import { buildSubagentInstructions } from "./subagent.ts";
describe("buildSubagentInstructions", () => {
it("includes system preamble, resolved context, and orchestrator prompt", () => {
const prompt = "Read file.ts and fix the type error.";
const ctx = {
repo: { owner: "test-owner", name: "test-repo" },
} as any;
const instructions = buildSubagentInstructions({
ctx,
label: "test-task",
instructions: prompt,
});
expect(instructions.user).toBe(prompt);
expect(instructions.full).toContain("[CONTEXT]");
expect(instructions.full).toContain("test-owner/test-repo");
expect(instructions.full).toContain("subagent_label: test-task");
expect(instructions.full).toContain("set_output");
expect(instructions.full).toContain(prompt);
});
});
// ─── per-server tool isolation integration test ─────────────────────────
// demonstrates the architecture: orchestrator and subagent get separate servers
function getRandomPort(): Promise<number> {
return new Promise((resolve, reject) => {
const srv = createServer();
srv.listen(0, "127.0.0.1", () => {
const addr = srv.address();
if (!addr || typeof addr === "string") return reject(new Error("bad address"));
const port = addr.port;
srv.close(() => resolve(port));
});
});
}
async function connectMcpClient(url: string): Promise<Client> {
const transport = new StreamableHTTPClientTransport(new URL(url));
const client = new Client({ name: "test-client", version: "0.0.1" });
// @ts-expect-error — exactOptionalPropertyTypes mismatch: SDK Transport.sessionId?: string vs StreamableHTTPClientTransport getter returning string | undefined
await client.connect(transport);
return client;
}
function mockTool(name: string, description: string) {
return tool({
name,
description,
parameters: type({ value: "string" }),
execute: execute(async () => ({ ok: true })),
});
}
describe("per-server tool isolation - integration", () => {
let orchestratorServer: FastMCP;
let subagentServer: FastMCP;
let orchestratorUrl: string;
let subagentUrl: string;
const clients: Client[] = [];
beforeAll(async () => {
const [orchestratorPort, subagentPort] = await Promise.all([getRandomPort(), getRandomPort()]);
orchestratorUrl = `http://127.0.0.1:${orchestratorPort}/mcp`;
subagentUrl = `http://127.0.0.1:${subagentPort}/mcp`;
// orchestrator gets ALL tools (common + delegation + remote mutation)
orchestratorServer = new FastMCP({ name: "orchestrator", version: "0.0.1" });
orchestratorServer.addTool(mockTool("file_read", "read a file"));
orchestratorServer.addTool(mockTool("git", "run git commands"));
orchestratorServer.addTool(mockTool("set_output", "set output"));
orchestratorServer.addTool(mockTool("select_mode", "select a mode"));
orchestratorServer.addTool(mockTool("delegate", "delegate a task"));
orchestratorServer.addTool(mockTool("ask_question", "ask a question"));
orchestratorServer.addTool(mockTool("push_branch", "push branch"));
orchestratorServer.addTool(mockTool("create_pull_request", "create PR"));
// subagent gets ONLY file ops, shell, read-only GitHub, upload, set_output
subagentServer = new FastMCP({ name: "subagent", version: "0.0.1" });
subagentServer.addTool(mockTool("file_read", "read a file"));
subagentServer.addTool(mockTool("set_output", "set output"));
await Promise.all([
orchestratorServer.start({
transportType: "httpStream",
httpStream: { port: orchestratorPort, host: "127.0.0.1", endpoint: "/mcp" },
}),
subagentServer.start({
transportType: "httpStream",
httpStream: { port: subagentPort, host: "127.0.0.1", endpoint: "/mcp" },
}),
]);
});
afterAll(async () => {
for (const client of clients) {
try {
await client.close();
} catch {
// best-effort cleanup
}
}
await Promise.all([orchestratorServer.stop(), subagentServer.stop()]);
});
it("orchestrator sees all tools including delegation and mutation", async () => {
const client = await connectMcpClient(orchestratorUrl);
clients.push(client);
const result = await client.listTools();
const names = result.tools.map((t) => t.name);
expect(names).toContain("select_mode");
expect(names).toContain("delegate");
expect(names).toContain("ask_question");
expect(names).toContain("push_branch");
expect(names).toContain("create_pull_request");
expect(names).toContain("file_read");
expect(names).toContain("git");
expect(names).toContain("set_output");
expect(names.length).toBe(8);
});
it("subagent cannot see orchestrator-only tools", async () => {
const client = await connectMcpClient(subagentUrl);
clients.push(client);
const result = await client.listTools();
const names = result.tools.map((t) => t.name);
expect(names).not.toContain("select_mode");
expect(names).not.toContain("delegate");
expect(names).not.toContain("ask_question");
expect(names).not.toContain("push_branch");
expect(names).not.toContain("create_pull_request");
expect(names).not.toContain("git");
});
it("subagent sees only file ops, read-only tools, and set_output", async () => {
const client = await connectMcpClient(subagentUrl);
clients.push(client);
const result = await client.listTools();
const names = result.tools.map((t) => t.name);
expect(names).toContain("file_read");
expect(names).toContain("set_output");
expect(names.length).toBe(2);
});
});