From 9983e1406280b86e8eb019d2314e177f73c004e3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 05:57:49 +0000 Subject: [PATCH 1/6] test(provider-pi): reproduce #26822 underscore MCP alias unreachable on Pi These tests reproduce #26822: on Pi with tool exposure, the mcp__t3_code__ aliases are registered hidden, so a model call to them fails with "Tool ... not found". The fake Pi now resolves model calls against active, non-hidden tools and drops prepareLoadout hiddenDeclarations from what it declares. Co-Authored-By: Claude Opus 5.5 --- .../src/server/mcpBridge.testkit.ts | 53 +++++++++++++++++++ .../src/server/mcpExtensionSource.test.ts | 43 +++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/packages/provider-pi/src/server/mcpBridge.testkit.ts b/packages/provider-pi/src/server/mcpBridge.testkit.ts index 133a4b2981ac1..005a3977499e7 100644 --- a/packages/provider-pi/src/server/mcpBridge.testkit.ts +++ b/packages/provider-pi/src/server/mcpBridge.testkit.ts @@ -12,6 +12,12 @@ interface RegisteredTool { readonly exposure?: string; readonly promptSnippet?: string; readonly promptGuidelines?: ReadonlyArray; + readonly prepareLoadout?: (loadout: { + readonly declared: ReadonlyArray; + readonly callable: ReadonlyArray; + readonly registered: ReadonlyArray; + readonly getExposure: (name: string) => string; + }) => { readonly hiddenDeclarations?: ReadonlyArray } | undefined; readonly execute: ( id: string, args: Record, @@ -39,12 +45,20 @@ type AgentStartHook = ( }, ) => Promise<{ systemPrompt: string } | undefined>; +function activatesOnRegistration(tool: RegisteredTool) { + return ( + tool.exposure === undefined || tool.exposure === "direct" || tool.exposure === "model-only" + ); +} + export async function loadMcpBridge( options: { readonly modern?: boolean; readonly result?: unknown; readonly toolSearchAvailable?: boolean; readonly toolSearchDisabled?: boolean; + // Pi activates direct and model-only tools when they are registered. + readonly activatesOnRegistration?: boolean; readonly allowsTool?: (name: string) => boolean; readonly runtimeMode?: string; readonly inputSchema?: Record; @@ -128,8 +142,17 @@ export async function loadMcpBridge( registerTool: (tool: RegisteredTool) => { if (options.allowsTool && !options.allowsTool(tool.name)) return; const index = tools.findIndex((current) => current.name === tool.name); + const previous = index === -1 ? undefined : tools[index]; if (index === -1) tools.push(tool); else tools[index] = tool; + if ( + options.activatesOnRegistration && + activatesOnRegistration(tool) && + (previous === undefined || !activatesOnRegistration(previous)) && + !activeTools.includes(tool.name) + ) { + activeTools = [...activeTools, tool.name]; + } }, getActiveTools: () => activeTools, setActiveTools: (names: string[]) => { @@ -152,9 +175,39 @@ export async function loadMcpBridge( : {}), }, }); + const exposureOf = (name: string) => + tools.find((tool) => tool.name === name)?.exposure ?? "direct"; + // Pi resolves model tool calls against the active, non-hidden tools only. + const modelTools = () => + tools.filter((tool) => activeTools.includes(tool.name) && exposureOf(tool.name) !== "hidden"); + const declaredTools = () => { + const active = modelTools(); + const loadout = { + declared: active, + callable: tools.filter( + (tool) => + ["codemode", "deferred"].includes(exposureOf(tool.name)) || + (exposureOf(tool.name) === "direct" && active.includes(tool)), + ), + registered: tools, + getExposure: exposureOf, + }; + const hidden = new Set( + active.flatMap((tool) => tool.prepareLoadout?.(loadout)?.hiddenDeclarations ?? []), + ); + return active.map((tool) => tool.name).filter((name) => !hidden.has(name)); + }; return { handlers, tools, + declaredTools, + callModelTool: async (name: string, args: Record) => { + const tool = modelTools().find((candidate) => candidate.name === name); + if (tool === undefined) { + return { content: [{ type: "text", text: `Tool ${name} not found` }], isError: true }; + } + return tool.execute("model-call", args); + }, setBridgeSourcePath: (path: string) => { bridgeSourcePath = path; }, diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index b1306f701e27a..4b3f15e3fdc9e 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -200,6 +200,49 @@ describe("Pi MCP tool exposure", () => { assert.include(bridge.getActiveTools(), "mcp__t3-code__preview_snapshot"); }); + it("keeps Pi's canonical MCP tool names callable by the model without declaring them twice", async () => { + const bridge = await loadMcpBridge({ + modern: true, + toolSearchAvailable: true, + activatesOnRegistration: true, + }); + const canonicalNames = [ + "mcp__t3_code__orchestrator_capabilities", + "mcp__t3_code__delegate_task", + "mcp__t3_code__task_status", + "mcp__t3_code__preview_snapshot", + ]; + const unreachableCanonicalNames = async () => { + const unreachable: string[] = []; + for (const name of canonicalNames) { + const result = await bridge.callModelTool(name, {}); + if (result.isError === true) unreachable.push(name); + } + return unreachable; + }; + const declaredCanonicalNames = () => + bridge.declaredTools().filter((name) => name.startsWith("mcp__t3_code__")); + const event = { systemPrompt: "" }; + + await bridge.handlers.get("session_start")!(event, discoveryContext); + assert.deepEqual( + await unreachableCanonicalNames(), + [], + "model calls to these names fail with Tool not found after session start", + ); + assert.deepEqual(declaredCanonicalNames(), [], "declared twice after session start"); + assert.equal(bridge.requests.at(-1)?.method, "tools/call"); + + bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); + await bridge.handlers.get("session_tree")!(event, discoveryContext); + assert.deepEqual( + await unreachableCanonicalNames(), + [], + "model calls to these names fail with Tool not found after an older tree loadout", + ); + assert.deepEqual(declaredCanonicalNames(), [], "declared twice after an older tree loadout"); + }); + it.each(["legacy Pi", "disabled tool search"])( "keeps tool execution available with %s", async (mode) => { From b51ded26f3c88ebf3f6cda61a5c6a3cac5e0308b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 06:03:28 +0000 Subject: [PATCH 2/6] fix(provider-pi): Pi's mcp__t3_code__ tool names are callable again On Pi with tool exposure the bridge registered each T3 tool's canonical mcp__t3_code__ alias as hidden, which Pi treats as unreachable, so a model that called it got "Tool ... not found". The alias is now direct and hides its own declaration through prepareLoadout, so it stays active and callable without a second declaration or a second tool_search result. Discovery reconciliation re-adds aliases dropped by older tree loadouts and keeps an alias inactive when --exclude-tools removed its public tool. Fixes pingdotgg/t3code#26822 Co-Authored-By: Claude Opus 5.5 --- .../src/server/mcpExtensionSource.test.ts | 42 +++++++++++++------ .../src/server/mcpExtensionSource.ts | 30 +++++++++---- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 4b3f15e3fdc9e..419ac21cb7554 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -145,10 +145,17 @@ describe("Pi MCP tool exposure", () => { assert.include(prompt?.systemPrompt ?? "", "orchestrator_capabilities"); assert.equal(bridge.servers.length, 0); assert.equal(bridge.tools.length, 8); - assert.deepEqual(bridge.getActiveTools(), ["read", "tool_search"]); + const aliases = [ + "mcp__t3_code__orchestrator_capabilities", + "mcp__t3_code__delegate_task", + "mcp__t3_code__task_status", + "mcp__t3_code__preview_snapshot", + ]; + assert.deepEqual(bridge.getActiveTools(), ["read", "tool_search", ...aliases]); + assert.deepEqual(bridge.declaredTools(), []); assert.deepEqual( bridge.tools - .filter((tool) => tool.exposure !== "hidden") + .filter((tool) => tool.name.startsWith("mcp__t3-code__")) .map((tool) => [tool.name, tool.exposure]), [ ["mcp__t3-code__orchestrator_capabilities", "direct"], @@ -158,13 +165,10 @@ describe("Pi MCP tool exposure", () => { ], ); assert.deepEqual( - bridge.tools.filter((tool) => tool.exposure === "hidden").map((tool) => tool.name), - [ - "mcp__t3_code__orchestrator_capabilities", - "mcp__t3_code__delegate_task", - "mcp__t3_code__task_status", - "mcp__t3_code__preview_snapshot", - ], + bridge.tools + .filter((tool) => tool.name.startsWith("mcp__t3_code__")) + .map((tool) => [tool.name, tool.exposure]), + aliases.map((name) => [name, "direct"]), ); const result = await bridge.tools .find((tool) => tool.name === "mcp__t3-code__preview_snapshot")! @@ -191,6 +195,14 @@ describe("Pi MCP tool exposure", () => { "tool_search", "mcp__t3-code__orchestrator_capabilities", "mcp__t3-code__preview_snapshot", + "mcp__t3_code__orchestrator_capabilities", + "mcp__t3_code__task_status", + "mcp__t3_code__preview_snapshot", + ]); + assert.deepEqual(bridge.declaredTools(), [ + "mcp__t3-code__orchestrator_capabilities", + "mcp__t3-code__task_status", + "mcp__t3-code__preview_snapshot", ]); await bridge.handlers.get("model_select")!(event, discoveryContext); assert.equal( @@ -255,12 +267,18 @@ describe("Pi MCP tool exposure", () => { const start = bridge.handlers.get("session_start"); await start!({ systemPrompt: "Pi system prompt" }, discoveryContext); } - assert.equal(bridge.tools.filter((tool) => tool.exposure !== "hidden").length, 4); + const publicTools = bridge.tools.filter((tool) => tool.name.startsWith("mcp__t3-code__")); + assert.equal(publicTools.length, 4); assert.isTrue( + publicTools.every((tool) => tool.exposure === undefined || tool.exposure === "direct"), + ); + assert.deepEqual( bridge.tools - .filter((tool) => tool.exposure !== "hidden") - .every((tool) => tool.exposure === undefined || tool.exposure === "direct"), + .filter((tool) => tool.name.startsWith("mcp__t3_code__")) + .map((tool) => tool.exposure), + mode === "legacy Pi" ? [] : ["direct", "direct", "direct", "direct"], ); + assert.isFalse(bridge.declaredTools().some((name) => name.startsWith("mcp__t3_code__"))); const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot"); assert.isDefined(tool); const controller = new AbortController(); diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index dcc4a0230a8db..790414468fd03 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -349,6 +349,7 @@ export default async function t3McpExtension(pi: ExtensionAPI) { // bridge as the credential owner: mcp.json overrides native registrations. const supportsExposure = "registerMcpServer" in pi && typeof pi.registerMcpServer === "function"; const directTools = new Set(["orchestrator_capabilities", "delegate_task", "task_status"]); + const ALIAS_PREFIX = "mcp__t3_code__"; let deferOptionalTools = supportsExposure; let catalog: ReadonlyArray = []; @@ -356,23 +357,26 @@ export default async function t3McpExtension(pi: ExtensionAPI) { let started: Promise | undefined; const registerTools = () => { - // Preserve public names for saved loadouts and tool selectors. Hidden - // canonical names reserve ownership against Pi's configured MCP servers. - const prefixes = supportsExposure ? ["mcp__t3-code__", "mcp__t3_code__"] : ["mcp__t3-code__"]; + // Preserve public names for saved loadouts and tool selectors. The alias, + // Pi's own MCP name for T3, reserves it against Pi's configured MCP servers + // and stays active and callable without a second declaration. + const prefixes = supportsExposure ? ["mcp__t3-code__", ALIAS_PREFIX] : ["mcp__t3-code__"]; readOnlyMcpTools.clear(); for (const tool of catalog) { const name = tool.name; for (const prefix of prefixes) { - if (tool.annotations?.readOnlyHint === true) readOnlyMcpTools.add(\`\${prefix}\${name}\`); - const exposure = prefix === "mcp__t3_code__" ? "hidden" : - deferOptionalTools && !directTools.has(name) ? "deferred" : "direct"; + const toolName = \`\${prefix}\${name}\`; + if (tool.annotations?.readOnlyHint === true) readOnlyMcpTools.add(toolName); + const isAlias = prefix === ALIAS_PREFIX; + const exposure = !isAlias && deferOptionalTools && !directTools.has(name) ? "deferred" : "direct"; pi.registerTool({ - name: \`\${prefix}\${name}\`, + name: toolName, label: name, description: tool.description ?? name, parameters: jsonSchemaToTypebox(tool.inputSchema), ...(supportsExposure ? { outputSchema: mcpOutputSchema(tool) } : {}), ...(supportsExposure ? { exposure } : {}), + ...(isAlias ? { prepareLoadout: () => ({ hiddenDeclarations: [toolName] }) } : {}), async execute(_toolCallId, params, signal) { const result = await client.callTool( name, @@ -422,7 +426,14 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const exposureChanged = deferOptionalTools !== hasToolSearch; deferOptionalTools = hasToolSearch; if (exposureChanged) registerTools(); - const active = pi.getActiveTools(); + // Aliases are callable only while active. Keep one active exactly while + // --exclude-tools left its public tool, including after older tree restores. + const registered = new Set(pi.getAllTools().map((tool) => tool.name)); + const aliases = new Set(catalog + .filter((tool) => registered.has("mcp__t3-code__" + tool.name)) + .map((tool) => ALIAS_PREFIX + tool.name)); + const current = pi.getActiveTools(); + const active = current.filter((name) => !name.startsWith(ALIAS_PREFIX) || aliases.has(name)); // Completion gateways can silently drop calls to undeclared tools named // in history or guidance. Eagerly load T3 schemas, including after tree // restores, while retaining deferred exposure for Pi's allowlist rules. @@ -430,9 +441,10 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const required = [ ...(hasToolSearch ? ["tool_search"] : []), ...(ctx.model?.api === "openai-completions" ? catalog.map((tool) => "mcp__t3-code__" + tool.name) : []), + ...aliases, ]; const missing = required.filter((name) => !active.includes(name)); - if (missing.length > 0) pi.setActiveTools([...active, ...missing]); + if (missing.length > 0 || active.length !== current.length) pi.setActiveTools([...active, ...missing]); }; // Tree navigation restores its saved loadout after session_start. From 26738c4f15fab3c47e230ba1f99dc4f00b1e6a25 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 06:13:59 +0000 Subject: [PATCH 3/6] test(provider-pi): cover excluded aliases; docs: Pi aliases are callable Co-Authored-By: Claude Opus 5.5 --- docs/orchestration-v2/orchestrator-mcp-server.md | 8 +++++--- .../provider-pi/src/server/mcpExtensionSource.test.ts | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index 6bd4dea35d7c5..234ad2d942c09 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -160,9 +160,11 @@ T3_MCP_BEARER_TOKEN= ``` The extension preserves public names under `mcp__t3-code__` for saved loadouts -and tool selectors. Modern Pi also receives hidden `mcp__t3_code__` aliases, -which reserve the normalized namespace against configured MCP servers without -adding declarations or search results. On Pi 0.99+, +and tool selectors. Modern Pi also receives `mcp__t3_code__` aliases, Pi's own +name for the server, which reserve it against configured MCP servers. They stay +active and callable but hide their declarations, so they add no declarations or +search results, and an alias is dropped when its public tool is excluded. Pi's +`hidden` exposure would make them unreachable. On Pi 0.99+, `orchestrator_capabilities`, `delegate_task`, and `task_status` remain directly available; optional tools are discovered through Pi's builtin `tool_search`. On older Pi or without builtin search, all tools remain directly available. diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 419ac21cb7554..e49925e63bd73 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -182,9 +182,11 @@ describe("Pi MCP tool exposure", () => { modern: true, toolSearchAvailable: true, allowsTool: (name) => name !== "mcp__t3-code__delegate_task", + activatesOnRegistration: true, }); const event = { systemPrompt: "" }; await bridge.handlers.get("session_start")!(event, discoveryContext); + assert.notInclude(bridge.getActiveTools(), "mcp__t3_code__delegate_task"); const gatewayContext = { model: { api: "openai-completions" }, ui: { notify: assert.fail } }; await bridge.handlers.get("model_select")!(event, gatewayContext); bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); From eb5723f4c906a423198b9006bb3c08c39e03c71d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 06:32:02 +0000 Subject: [PATCH 4/6] fix(provider-pi): keep Pi's T3 aliases out of codemode and behind their public tool Co-Authored-By: Claude Opus 5.5 --- .../orchestrator-mcp-server.md | 9 ++-- .../src/server/mcpBridge.testkit.ts | 14 +++--- .../src/server/mcpExtensionSource.test.ts | 43 ++++++++++++++++--- .../src/server/mcpExtensionSource.ts | 33 ++++++++------ 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index 234ad2d942c09..3081744af7bda 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -161,10 +161,11 @@ T3_MCP_BEARER_TOKEN= The extension preserves public names under `mcp__t3-code__` for saved loadouts and tool selectors. Modern Pi also receives `mcp__t3_code__` aliases, Pi's own -name for the server, which reserve it against configured MCP servers. They stay -active and callable but hide their declarations, so they add no declarations or -search results, and an alias is dropped when its public tool is excluded. Pi's -`hidden` exposure would make them unreachable. On Pi 0.99+, +name for the server, which reserve it against configured MCP servers. They are +`model-only` and hide their own declarations, so the model can call them without +added declarations, search results, or codemode entries. An alias follows its +public tool: dropped when that tool is excluded, and off while a direct one is +off. Pi's `hidden` exposure would make them unreachable. On Pi 0.99+, `orchestrator_capabilities`, `delegate_task`, and `task_status` remain directly available; optional tools are discovered through Pi's builtin `tool_search`. On older Pi or without builtin search, all tools remain directly available. diff --git a/packages/provider-pi/src/server/mcpBridge.testkit.ts b/packages/provider-pi/src/server/mcpBridge.testkit.ts index 005a3977499e7..330eafd27bd0b 100644 --- a/packages/provider-pi/src/server/mcpBridge.testkit.ts +++ b/packages/provider-pi/src/server/mcpBridge.testkit.ts @@ -180,15 +180,18 @@ export async function loadMcpBridge( // Pi resolves model tool calls against the active, non-hidden tools only. const modelTools = () => tools.filter((tool) => activeTools.includes(tool.name) && exposureOf(tool.name) !== "hidden"); + // Pi's ctx.executeTool() catalog, which codemode scripts search and list. + const callableTools = () => + tools.filter( + (tool) => + ["codemode", "deferred"].includes(exposureOf(tool.name)) || + (exposureOf(tool.name) === "direct" && activeTools.includes(tool.name)), + ); const declaredTools = () => { const active = modelTools(); const loadout = { declared: active, - callable: tools.filter( - (tool) => - ["codemode", "deferred"].includes(exposureOf(tool.name)) || - (exposureOf(tool.name) === "direct" && active.includes(tool)), - ), + callable: callableTools(), registered: tools, getExposure: exposureOf, }; @@ -201,6 +204,7 @@ export async function loadMcpBridge( handlers, tools, declaredTools, + callableTools: () => callableTools().map((tool) => tool.name), callModelTool: async (name: string, args: Record) => { const tool = modelTools().find((candidate) => candidate.name === name); if (tool === undefined) { diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index e49925e63bd73..54d18e9ce8bd2 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -137,7 +137,11 @@ describe("Pi MCP tool exposure", () => { }; it("keeps orchestration direct and optional bridge tools discoverable on modern Pi", async () => { - const bridge = await loadMcpBridge({ modern: true, toolSearchAvailable: true }); + const bridge = await loadMcpBridge({ + modern: true, + toolSearchAvailable: true, + activatesOnRegistration: true, + }); await bridge.handlers.get("session_start")!({ systemPrompt: "" }, discoveryContext); const start = bridge.handlers.get("before_agent_start"); assert.isDefined(start); @@ -151,8 +155,18 @@ describe("Pi MCP tool exposure", () => { "mcp__t3_code__task_status", "mcp__t3_code__preview_snapshot", ]; - assert.deepEqual(bridge.getActiveTools(), ["read", "tool_search", ...aliases]); - assert.deepEqual(bridge.declaredTools(), []); + const publicDirect = [ + "mcp__t3-code__orchestrator_capabilities", + "mcp__t3-code__delegate_task", + "mcp__t3-code__task_status", + ]; + assert.sameMembers(bridge.getActiveTools(), [ + "read", + "tool_search", + ...publicDirect, + ...aliases, + ]); + assert.deepEqual(bridge.declaredTools(), publicDirect); assert.deepEqual( bridge.tools .filter((tool) => tool.name.startsWith("mcp__t3-code__")) @@ -168,7 +182,7 @@ describe("Pi MCP tool exposure", () => { bridge.tools .filter((tool) => tool.name.startsWith("mcp__t3_code__")) .map((tool) => [tool.name, tool.exposure]), - aliases.map((name) => [name, "direct"]), + aliases.map((name) => [name, "model-only"]), ); const result = await bridge.tools .find((tool) => tool.name === "mcp__t3-code__preview_snapshot")! @@ -247,7 +261,12 @@ describe("Pi MCP tool exposure", () => { assert.deepEqual(declaredCanonicalNames(), [], "declared twice after session start"); assert.equal(bridge.requests.at(-1)?.method, "tools/call"); - bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); + bridge.restoreActiveTools([ + "read", + "mcp__t3-code__orchestrator_capabilities", + "mcp__t3-code__delegate_task", + "mcp__t3-code__task_status", + ]); await bridge.handlers.get("session_tree")!(event, discoveryContext); assert.deepEqual( await unreachableCanonicalNames(), @@ -255,6 +274,18 @@ describe("Pi MCP tool exposure", () => { "model calls to these names fail with Tool not found after an older tree loadout", ); assert.deepEqual(declaredCanonicalNames(), [], "declared twice after an older tree loadout"); + assert.isFalse( + bridge.callableTools().some((name) => name.startsWith("mcp__t3_code__")), + "aliases duplicate the codemode catalog", + ); + + bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); + await bridge.handlers.get("session_tree")!(event, discoveryContext); + assert.deepEqual( + await unreachableCanonicalNames(), + ["mcp__t3_code__orchestrator_capabilities", "mcp__t3_code__delegate_task"], + "an alias stays callable while its direct public tool is off", + ); }); it.each(["legacy Pi", "disabled tool search"])( @@ -278,7 +309,7 @@ describe("Pi MCP tool exposure", () => { bridge.tools .filter((tool) => tool.name.startsWith("mcp__t3_code__")) .map((tool) => tool.exposure), - mode === "legacy Pi" ? [] : ["direct", "direct", "direct", "direct"], + mode === "legacy Pi" ? [] : ["model-only", "model-only", "model-only", "model-only"], ); assert.isFalse(bridge.declaredTools().some((name) => name.startsWith("mcp__t3_code__"))); const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot"); diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index 790414468fd03..5aca0bb44e53b 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -351,6 +351,7 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const directTools = new Set(["orchestrator_capabilities", "delegate_task", "task_status"]); const ALIAS_PREFIX = "mcp__t3_code__"; let deferOptionalTools = supportsExposure; + const isDeferred = (name: string) => deferOptionalTools && !directTools.has(name); let catalog: ReadonlyArray = []; const client = createMcpClient(endpoint, token); @@ -358,8 +359,8 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const registerTools = () => { // Preserve public names for saved loadouts and tool selectors. The alias, - // Pi's own MCP name for T3, reserves it against Pi's configured MCP servers - // and stays active and callable without a second declaration. + // Pi's own MCP name for T3, reserves it against Pi's configured MCP servers. + // model-only keeps it out of codemode; it hides its own declaration. const prefixes = supportsExposure ? ["mcp__t3-code__", ALIAS_PREFIX] : ["mcp__t3-code__"]; readOnlyMcpTools.clear(); for (const tool of catalog) { @@ -368,7 +369,7 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const toolName = \`\${prefix}\${name}\`; if (tool.annotations?.readOnlyHint === true) readOnlyMcpTools.add(toolName); const isAlias = prefix === ALIAS_PREFIX; - const exposure = !isAlias && deferOptionalTools && !directTools.has(name) ? "deferred" : "direct"; + const exposure = isAlias ? "model-only" : isDeferred(name) ? "deferred" : "direct"; pi.registerTool({ name: toolName, label: name, @@ -426,14 +427,6 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const exposureChanged = deferOptionalTools !== hasToolSearch; deferOptionalTools = hasToolSearch; if (exposureChanged) registerTools(); - // Aliases are callable only while active. Keep one active exactly while - // --exclude-tools left its public tool, including after older tree restores. - const registered = new Set(pi.getAllTools().map((tool) => tool.name)); - const aliases = new Set(catalog - .filter((tool) => registered.has("mcp__t3-code__" + tool.name)) - .map((tool) => ALIAS_PREFIX + tool.name)); - const current = pi.getActiveTools(); - const active = current.filter((name) => !name.startsWith(ALIAS_PREFIX) || aliases.has(name)); // Completion gateways can silently drop calls to undeclared tools named // in history or guidance. Eagerly load T3 schemas, including after tree // restores, while retaining deferred exposure for Pi's allowlist rules. @@ -441,10 +434,22 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const required = [ ...(hasToolSearch ? ["tool_search"] : []), ...(ctx.model?.api === "openai-completions" ? catalog.map((tool) => "mcp__t3-code__" + tool.name) : []), - ...aliases, ]; - const missing = required.filter((name) => !active.includes(name)); - if (missing.length > 0 || active.length !== current.length) pi.setActiveTools([...active, ...missing]); + const current = pi.getActiveTools(); + const missing = required.filter((name) => !current.includes(name)); + if (missing.length > 0) pi.setActiveTools([...current, ...missing]); + // An alias is callable only while active. It follows its public tool: + // gone when --exclude-tools removed it, off while a direct one is off. + const registered = new Set(pi.getAllTools().map((tool) => tool.name)); + const loaded = pi.getActiveTools(); + const aliases = catalog + .filter((tool) => registered.has(ALIAS_PREFIX + tool.name) && + registered.has("mcp__t3-code__" + tool.name) && + (isDeferred(tool.name) || loaded.includes("mcp__t3-code__" + tool.name))) + .map((tool) => ALIAS_PREFIX + tool.name); + const kept = loaded.filter((name) => !name.startsWith(ALIAS_PREFIX) || aliases.includes(name)); + const added = aliases.filter((name) => !loaded.includes(name)); + if (added.length > 0 || kept.length !== loaded.length) pi.setActiveTools([...kept, ...added]); }; // Tree navigation restores its saved loadout after session_start. From 0b41d1149cf85f430dc6cb87de6764f3c7961330 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 06:59:39 +0000 Subject: [PATCH 5/6] fix(provider-pi): check an alias's public tool when the alias runs Co-Authored-By: Claude Opus 5.5 --- .../src/server/mcpExtensionSource.test.ts | 8 ++++++++ .../provider-pi/src/server/mcpExtensionSource.ts | 15 ++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 54d18e9ce8bd2..6b9e593096be9 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -279,6 +279,14 @@ describe("Pi MCP tool exposure", () => { "aliases duplicate the codemode catalog", ); + const requestCount = bridge.requests.length; + bridge.restoreActiveTools( + bridge.getActiveTools().filter((name) => name !== "mcp__t3-code__delegate_task"), + ); + const late = await bridge.callModelTool("mcp__t3_code__delegate_task", {}); + assert.isTrue(late.isError, "an alias runs after its public tool was switched off"); + assert.equal(bridge.requests.length, requestCount); + bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); await bridge.handlers.get("session_tree")!(event, discoveryContext); assert.deepEqual( diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index 5aca0bb44e53b..a976bf5325a34 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -352,6 +352,10 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const ALIAS_PREFIX = "mcp__t3_code__"; let deferOptionalTools = supportsExposure; const isDeferred = (name: string) => deferOptionalTools && !directTools.has(name); + // An alias follows its public tool: gone when --exclude-tools removed it, + // off while a direct one is off. + const followsPublicTool = (name: string, registered: ReadonlySet, active: ReadonlyArray) => + registered.has("mcp__t3-code__" + name) && (isDeferred(name) || active.includes("mcp__t3-code__" + name)); let catalog: ReadonlyArray = []; const client = createMcpClient(endpoint, token); @@ -379,6 +383,10 @@ export default async function t3McpExtension(pi: ExtensionAPI) { ...(supportsExposure ? { exposure } : {}), ...(isAlias ? { prepareLoadout: () => ({ hiddenDeclarations: [toolName] }) } : {}), async execute(_toolCallId, params, signal) { + // Another extension may switch the public tool off after reconciliation. + if (isAlias && !followsPublicTool(name, new Set(pi.getAllTools().map((tool) => tool.name)), pi.getActiveTools())) { + return { content: [{ type: "text", text: \`Tool \${toolName} not found\` }], details: {}, isError: true }; + } const result = await client.callTool( name, (params ?? {}) as Record, @@ -438,14 +446,11 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const current = pi.getActiveTools(); const missing = required.filter((name) => !current.includes(name)); if (missing.length > 0) pi.setActiveTools([...current, ...missing]); - // An alias is callable only while active. It follows its public tool: - // gone when --exclude-tools removed it, off while a direct one is off. + // An alias is callable only while active. const registered = new Set(pi.getAllTools().map((tool) => tool.name)); const loaded = pi.getActiveTools(); const aliases = catalog - .filter((tool) => registered.has(ALIAS_PREFIX + tool.name) && - registered.has("mcp__t3-code__" + tool.name) && - (isDeferred(tool.name) || loaded.includes("mcp__t3-code__" + tool.name))) + .filter((tool) => registered.has(ALIAS_PREFIX + tool.name) && followsPublicTool(tool.name, registered, loaded)) .map((tool) => ALIAS_PREFIX + tool.name); const kept = loaded.filter((name) => !name.startsWith(ALIAS_PREFIX) || aliases.includes(name)); const added = aliases.filter((name) => !loaded.includes(name)); From 0c300c0652592fd5864c622487cf759c89904101 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 00:42:43 +0000 Subject: [PATCH 6/6] refactor(provider-pi): wrap alias guard lines and drop duplicate declaration asserts AI-assisted; verified with the tests and tools listed above. --- .../src/server/mcpExtensionSource.test.ts | 6 ------ .../provider-pi/src/server/mcpExtensionSource.ts | 14 +++++++++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 18af2dac5c1ee..aa21fea07c953 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -247,11 +247,6 @@ describe("Pi MCP tool exposure", () => { "mcp__t3_code__task_status", "mcp__t3_code__preview_snapshot", ]); - assert.deepEqual(bridge.declaredTools(), [ - "mcp__t3-code__orchestrator_capabilities", - "mcp__t3-code__task_status", - "mcp__t3-code__preview_snapshot", - ]); await bridge.handlers.get("model_select")!(event, discoveryContext); assert.equal( bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot")?.exposure, @@ -351,7 +346,6 @@ describe("Pi MCP tool exposure", () => { .map((tool) => tool.exposure), mode === "legacy Pi" ? [] : ["model-only", "model-only", "model-only", "model-only"], ); - assert.isFalse(bridge.declaredTools().some((name) => name.startsWith("mcp__t3_code__"))); const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot"); assert.isDefined(tool); const controller = new AbortController(); diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index 93b7046aa9b66..c580a1d096dec 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -390,8 +390,13 @@ export default async function t3McpExtension(pi: ExtensionAPI) { const isDeferred = (name: string) => deferOptionalTools && !directTools.has(name); // An alias follows its public tool: gone when --exclude-tools removed it, // off while a direct one is off. - const followsPublicTool = (name: string, registered: ReadonlySet, active: ReadonlyArray) => - registered.has("mcp__t3-code__" + name) && (isDeferred(name) || active.includes("mcp__t3-code__" + name)); + const followsPublicTool = ( + name: string, + registered: ReadonlySet, + active: ReadonlyArray, + ) => + registered.has("mcp__t3-code__" + name) && + (isDeferred(name) || active.includes("mcp__t3-code__" + name)); let catalog: ReadonlyArray = []; const client = createMcpClient(endpoint, token); @@ -420,7 +425,10 @@ export default async function t3McpExtension(pi: ExtensionAPI) { ...(isAlias ? { prepareLoadout: () => ({ hiddenDeclarations: [toolName] }) } : {}), async execute(_toolCallId, params, signal) { // Another extension may switch the public tool off after reconciliation. - if (isAlias && !followsPublicTool(name, new Set(pi.getAllTools().map((tool) => tool.name)), pi.getActiveTools())) { + if ( + isAlias && + !followsPublicTool(name, new Set(pi.getAllTools().map((tool) => tool.name)), pi.getActiveTools()) + ) { return { content: [{ type: "text", text: \`Tool \${toolName} not found\` }], details: {}, isError: true }; } const result = await client.callTool(