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