1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6
7import {
8 PI_T3_MCP_EXTENSION_FILENAME,
9 T3_MCP_BEARER_ENV,
10 T3_MCP_URL_ENV,
11 T3_PI_RUNTIME_MODE_ENV,
12 T3_PI_MCP_EXTENSION_PATH_ENV,
13} from "./mcpExtensionSource.ts";
14import {
15 buildPiRpcLaunch,
16 materializePiT3McpExtension,
17 resolvePiLaunchArgs,
18} from "./mcpInjection.ts";
19
20const threadId = ThreadId.make("thread-pi-t3-mcp");
21
22const mcpSession = {
23 environmentId: EnvironmentId.make("environment-pi-t3-mcp"),
24 threadId,
25 providerSessionId: "mcp-session-pi",
26 providerInstanceId: ProviderInstanceId.make("pi"),
27 endpoint: "http://127.0.0.1:43123/mcp",
28 authorizationHeader: "Bearer secret-pi-token",
29 browserToolsAvailable: true,
30};
31
32describe("pi T3 MCP injection", () => {
33 it("always adds the permission bridge and configures MCP when available", () => {
34 const resolvedArgs = resolvePiLaunchArgs(
35 "--extension=/home/user/.pi/agent/extensions/demo.ts --session-dir=/tmp/pi-sessions --provider=anthropic --model=claude-sonnet --tools='' --name=-review --extension-flag=kept",
36 );
37 assert.isTrue(resolvedArgs.ok);
38 if (!resolvedArgs.ok) return;
39 const launch = buildPiRpcLaunch({
40 launchArgs: resolvedArgs.args,
41 environment: { PATH: "/usr/bin" },
42 mcpSession,
43 extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts",
44 runtimeMode: "approval-required",
45 });
46 assert.deepEqual(launch.args, [
47 "--mode",
48 "rpc",
49 "--extension",
50 "/home/user/.pi/agent/extensions/demo.ts",
51 "--session-dir",
52 "/tmp/pi-sessions",
53 "--provider",
54 "anthropic",
55 "--model",
56 "claude-sonnet",
57 "--tools",
58 "",
59 "--name",
60 "-review",
61 "--extension-flag=kept",
62 "--extension",
63 "/tmp/cache/pi-t3-mcp-extension.ts",
64 ]);
65 assert.notInclude(launch.args, "--no-extensions");
66 assert.equal(launch.env[T3_MCP_URL_ENV], "http://127.0.0.1:43123/mcp");
67 assert.equal(launch.env[T3_MCP_BEARER_ENV], "secret-pi-token");
68 assert.equal(launch.env[T3_PI_RUNTIME_MODE_ENV], "approval-required");
69 assert.equal(launch.env[T3_PI_MCP_EXTENSION_PATH_ENV], "/tmp/cache/pi-t3-mcp-extension.ts");
70
71 const permissionOnly = buildPiRpcLaunch({
72 launchArgs: [],
73 environment: {
74 [T3_MCP_URL_ENV]: "http://127.0.0.1:9999/stale",
75 [T3_MCP_BEARER_ENV]: "stale-token",
76 [T3_PI_MCP_EXTENSION_PATH_ENV]: "/stale/extension.ts",
77 },
78 mcpSession: undefined,
79 extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts",
80 runtimeMode: "auto-accept-edits",
81 });
82 assert.deepEqual(permissionOnly.args, [
83 "--mode",
84 "rpc",
85 "--extension",
86 "/tmp/cache/pi-t3-mcp-extension.ts",
87 ]);
88 assert.isFalse(permissionOnly.hasT3Mcp);
89 assert.isUndefined(permissionOnly.env[T3_MCP_URL_ENV]);
90 assert.isUndefined(permissionOnly.env[T3_MCP_BEARER_ENV]);
91 assert.equal(permissionOnly.env[T3_PI_RUNTIME_MODE_ENV], "auto-accept-edits");
92 assert.equal(
93 permissionOnly.env[T3_PI_MCP_EXTENSION_PATH_ENV],
94 "/tmp/cache/pi-t3-mcp-extension.ts",
95 );
96 });
97
98 it("falls back to Pi's first supported mode for legacy auto threads", () => {
99 const launch = buildPiRpcLaunch({
100 launchArgs: [],
101 environment: {},
102 mcpSession: undefined,
103 extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts",
104 runtimeMode: "auto",
105 });
106
107 assert.equal(launch.env[T3_PI_RUNTIME_MODE_ENV], "approval-required");
108 });
109
110 it("forces tools and user extensions off for unattended text generation", () => {
111 const launch = buildPiRpcLaunch({
112 launchArgs: [
113 "--tools",
114 "read,write",
115 "--extension",
116 "/home/user/.pi/agent/extensions/demo.ts",
117 "--extension=./second.ts",
118 "--provider",
119 "anthropic",
120 ],
121 environment: {},
122 mcpSession,
123 extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts",
124 ephemeral: true,
125 disableExtensions: true,
126 disableTools: true,
127 });
128 assert.deepEqual(launch.args, [
129 "--mode",
130 "rpc",
131 "--no-session",
132 "--provider",
133 "anthropic",
134 "--no-extensions",
135 "--no-tools",
136 ]);
137 assert.isFalse(launch.hasT3Mcp);
138 assert.deepInclude(resolvePiLaunchArgs("--mode text"), {
139 ok: false,
140 message: "Pi launch argument '--mode' is controlled by T3 Code and cannot be overridden.",
141 });
142 assert.deepInclude(resolvePiLaunchArgs("--session old.jsonl"), { ok: false });
143 assert.deepInclude(resolvePiLaunchArgs("prompt pi immediately"), { ok: false });
144 assert.deepInclude(resolvePiLaunchArgs("--plan @instructions.md"), { ok: false });
145 });
146
147 it("rejects --provider without --model, which Pi 1.0 refuses at startup", () => {
148 const rejection = {
149 ok: false,
150 message: "Pi launch argument '--provider' requires '--model'.",
151 };
152 assert.deepInclude(resolvePiLaunchArgs("--provider openrouter"), rejection);
153 assert.deepInclude(resolvePiLaunchArgs("--provider=openrouter --models gpt-6"), rejection);
154 assert.isTrue(resolvePiLaunchArgs("--provider openrouter --model=deepseek/v4").ok);
155 assert.isTrue(resolvePiLaunchArgs("--model deepseek/v4").ok);
156 });
157
158 it.effect("materializes the MCP bridge with namespaced tool registration", () =>
159 Effect.gen(function* () {
160 const fs = yield* FileSystem.FileSystem;
161 const cacheDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pi-extensions-" });
162 const mcpDest = yield* materializePiT3McpExtension(cacheDir);
163 assert.isTrue(mcpDest.endsWith(PI_T3_MCP_EXTENSION_FILENAME));
164 const mcpSource = yield* fs.readFileString(mcpDest);
165 assert.include(mcpSource, "export default async function t3McpExtension");
166 assert.include(mcpSource, "before_agent_start");
167 assert.include(mcpSource, 'pi.on("tool_call"');
168 assert.include(mcpSource, "Allow ${event.toolName}?");
169 assert.include(mcpSource, '"mcp-protocol-version"');
170 assert.include(mcpSource, '"tools/call"');
171 assert.include(mcpSource, "mcp__t3-code__");
172 assert.include(mcpSource, "mcp__t3_code__");
173 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
174 );
175});