Skip to content
Source
Sign inSign up

mcpBridge.testkit.ts

172 lines5.7 KB

1// @effect-diagnostics nodeBuiltinImport:off -- Executes the shipped Pi extension at its native JavaScript boundary.
2import * as NodeCrypto from "node:crypto";
3import * as NodeModule from "node:module";
4import * as NodePath from "node:path";
5import * as NodeVM from "node:vm";
6import { PI_T3_MCP_EXTENSION_SOURCE } from "./mcpExtensionSource.ts";
7​
8interface RegisteredTool {
9 readonly name: string;
10 readonly description: string;
11 readonly parameters: unknown;
12 readonly outputSchema?: unknown;
13 readonly exposure?: string;
14 readonly promptSnippet?: string;
15 readonly promptGuidelines?: ReadonlyArray<string>;
16 readonly execute: (
17 id: string,
18 args: Record<string, unknown>,
19 signal?: AbortSignal,
20 ) => Promise<{
21 readonly content: ReadonlyArray<{
22 readonly type: string;
23 readonly text?: string;
24 readonly data?: string;
25 readonly mimeType?: string;
26 }>;
27 readonly structuredContent?: unknown;
28 readonly isError?: boolean;
29 }>;
31​
32type AgentStartHook = (
33 event: {
34 systemPrompt: string;
35 systemPromptOptions?: { sections?: Record<string, string> | null; forceSystemPrompt?: string };
36 },
37 ctx: {
38 model?: { api: string };
39 ui: { notify: (message: string, severity: string) => void };
40 },
41) => Promise<{ systemPrompt: string } | undefined>;
42​
43export async function loadMcpBridge(
44 options: {
45 readonly modern?: boolean;
46 readonly result?: unknown;
47 readonly toolSearchAvailable?: boolean;
48 readonly toolSearchDisabled?: boolean;
49 readonly allowsTool?: (name: string) => boolean;
50 readonly runtimeMode?: string;
51 readonly inputSchema?: Record<string, unknown>;
52 } = {},
53) {
54 const handlers = new Map<string, AgentStartHook>();
55 const tools: RegisteredTool[] = [];
56 const requests: Array<{ readonly method: string; readonly params?: unknown }> = [];
57 let activeTools = ["read"];
58 let bridgeSourcePath = "/fixture/pi-t3-extension.ts";
59 const transports: Array<{
60 readonly url: string;
61 readonly authorization: string;
62 readonly signal: AbortSignal | undefined;
63 }> = [];
64 const servers: Array<{ readonly name: string; readonly config: Record<string, unknown> }> = [];
65 const catalog = [
66 {
67 name: "orchestrator_capabilities",
68 description: "Discover available providers and models.",
69 annotations: { readOnlyHint: true },
70 },
71 { name: "delegate_task", description: "Delegate work to another agent." },
72 {
73 name: "task_status",
74 description: "Check delegated work.",
75 annotations: { readOnlyHint: false },
76 },
77 { name: "preview_snapshot", description: "Inspect the collaborative browser." },
78 ].map((tool) => ({
79 ...tool,
80 inputSchema: options.inputSchema ?? { type: "object", properties: {} },
81 outputSchema: { type: "object", properties: { count: { type: "number" } } },
82 }));
83 const source = NodeModule.stripTypeScriptTypes(
84 PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
85 "export default async function",
86 "async function",
87 ),
88 );
89 await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, {
90 process: {
91 env: {
92 T3_MCP_URL: "http://fixture.invalid/mcp",
93 T3_MCP_BEARER_TOKEN: "fixture-token",
94 T3_PI_MCP_EXTENSION_PATH: "/fixture/pi-t3-extension.ts",
95 T3_PI_RUNTIME_MODE: options.runtimeMode,
96 },
97 },
98 AbortSignal,
99 NodeCrypto,
100 NodePath,
101 Type: { Unsafe: (schema: unknown) => schema },
102 fetch: async (
103 url: string,
104 transport: { body: string; headers: Record<string, string>; signal?: AbortSignal },
105 ) => {
106 transport.signal?.throwIfAborted();
107 transports.push({
108 url,
109 authorization: transport.headers.authorization!,
110 signal: transport.signal,
111 });
112 const request = JSON.parse(transport.body) as {
113 id: number;
114 method: string;
115 params?: unknown;
116 };
117 requests.push(request);
118 const result =
119 request.method === "tools/list"
120 ? { tools: catalog }
121 : request.method === "tools/call"
122 ? (options.result ?? { content: [{ type: "text", text: "browser snapshot" }] })
123 : {};
124 return new Response(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }), {
125 headers: { "content-type": "application/json" },
126 });
127 },
128 pi: {
129 on: (name: string, handler: AgentStartHook) => handlers.set(name, handler),
130 registerCommand: () => {},
131 registerTool: (tool: RegisteredTool) => {
132 if (options.allowsTool && !options.allowsTool(tool.name)) return;
133 const index = tools.findIndex((current) => current.name === tool.name);
134 if (index === -1) tools.push(tool);
135 else tools[index] = tool;
136 },
137 getActiveTools: () => activeTools,
138 setActiveTools: (names: string[]) => {
139 activeTools = names.filter((name) => options.allowsTool?.(name) ?? true);
140 },
141 getAllTools: () => [
142 ...tools.map((tool) => ({ name: tool.name, sourceInfo: { path: bridgeSourcePath } })),
143 ...(options.toolSearchAvailable &&
144 !options.toolSearchDisabled &&
145 (options.allowsTool?.("tool_search") ?? true)
146 ? [{ name: "tool_search", sourceInfo: { path: "builtin:tool-search" } }]
147 : []),
148 ],
149 ...(options.modern
150 ? {
151 registerMcpServer: (name: string, config: Record<string, unknown>) =>
152 servers.push({ name, config }),
153 unregisterMcpServer: () => servers.splice(0),
154 }
155 : {}),
156 },
157 });
158 return {
159 handlers,
160 tools,
161 setBridgeSourcePath: (path: string) => {
162 bridgeSourcePath = path;
163 },
164 requests,
165 servers,
166 transports,
167 getActiveTools: () => activeTools,
168 restoreActiveTools: (names: string[]) => {
169 activeTools = names;
170 },
171 };