authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-04 18:14:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-04 18:14:03-07:00
logec20db4a5a27de45f69f64ca05d17ec1bd96644d
tree52fbdad1a49c2b416272e4b8be39854c038d9136
parent745c225f92401ba0950770fa7c3ec67a7067dbeb
signature Signed by PGP key B5690EEEBB952194

fix(server): share MCP tool presentation across providers (#15475)

Co-authored-by: Bil0000 <62337003+Bil0000@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

19 files changed, 958 insertions(+), 107 deletions(-)

apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+86-1
...@@ -1008,9 +1008,12 @@ describe("AcpAdapterV2", () => {...@@ -1008,9 +1008,12 @@ describe("AcpAdapterV2", () => {
1008 );1008 );
1009 type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"];1009 type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"];
1010 let handler: Parameters<Runtime["handleSessionUpdate"]>[0] | undefined;1010 let handler: Parameters<Runtime["handleSessionUpdate"]>[0] | undefined;
1011 let createTerminal: Parameters<Runtime["handleCreateTerminal"]>[0] | undefined;
1011 const instanceId = ProviderInstanceId.make("devin-replay");1012 const instanceId = ProviderInstanceId.make("devin-replay");
1012 const adapter = makeAcpAdapterV2({1013 const adapter = makeAcpAdapterV2({
1013 instanceId,1014 instanceId,
1015 // Production Devin runs commands through client terminals.
1016 clientTerminals: { childProcessSpawner, shellCommands: true },
1014 crypto: yield* Crypto.Crypto,1017 crypto: yield* Crypto.Crypto,
1015 fileSystem: yield* FileSystem.FileSystem,1018 fileSystem: yield* FileSystem.FileSystem,
1016 idAllocator,1019 idAllocator,
...@@ -1031,9 +1034,18 @@ describe("AcpAdapterV2", () => {...@@ -1031,9 +1034,18 @@ describe("AcpAdapterV2", () => {
1031 Effect.sync(() => {1034 Effect.sync(() => {
1032 handler = next;1035 handler = next;
1033 }).pipe(Effect.andThen(runtime.handleSessionUpdate(next))),1036 }).pipe(Effect.andThen(runtime.handleSessionUpdate(next))),
1037 handleCreateTerminal: (next) =>
1038 Effect.sync(() => {
1039 createTerminal = next;
1040 }).pipe(Effect.andThen(runtime.handleCreateTerminal(next))),
1034 prompt: () =>1041 prompt: () =>
1035 Effect.gen(function* () {1042 Effect.gen(function* () {
1036 assert.isDefined(handler);1043 assert.isDefined(handler);
1044 assert.isDefined(createTerminal);
1045 const fallbackTerminal = yield* createTerminal(
1046 { sessionId: "mock-session-1", command: "true acp-mcp-call task_status {}" },
1047 { requestId: "child-mcp-terminal", method: "terminal/create" },
1048 );
1037 // Production thread 54aeb6d7 split after "(command". Metadata shapes1049 // Production thread 54aeb6d7 split after "(command". Metadata shapes
1038 // below were captured from live Devin sessions showy-mile/fragrant-chamomile.1050 // below were captured from live Devin sessions showy-mile/fragrant-chamomile.
1039 const updates = [1051 const updates = [
...@@ -1075,6 +1087,48 @@ describe("AcpAdapterV2", () => {...@@ -1075,6 +1087,48 @@ describe("AcpAdapterV2", () => {
1075 "cognition.ai/subagent_context": { parentAgentId: "child-a" },1087 "cognition.ai/subagent_context": { parentAgentId: "child-a" },
1076 },1088 },
1077 },1089 },
1090 {
1091 sessionUpdate: "tool_call",
1092 toolCallId: "child-weather",
1093 title: "Check weather",
1094 status: "completed",
1095 rawInput: { server: "weather", tool: "get_weather", city: "Berlin" },
1096 rawOutput: {
1097 result: {
1098 _meta: {
1099 source: { name: "Weather", logoUrl: "https://example.com/weather.png" },
1100 },
1101 content: [{ type: "text", text: "Sunny" }],
1102 },
1103 },
1104 _meta: {
1105 is_mcp_tool_call: true,
1106 "cognition.ai/subagent_context": { parentAgentId: "child-a" },
1107 },
1108 },
1109 {
1110 sessionUpdate: "tool_call",
1111 toolCallId: "child-mcp-fallback",
1112 title: "Ran command",
1113 kind: "execute",
1114 status: "completed",
1115 content: [{ type: "terminal", terminalId: fallbackTerminal.terminalId }],
1116 _meta: { "cognition.ai/subagent_context": { parentAgentId: "child-a" } },
1117 },
1118 {
1119 sessionUpdate: "tool_call",
1120 toolCallId: "parent-weather",
1121 title: "Check weather",
1122 status: "completed",
1123 rawInput: { server: "weather", tool: "get_weather", city: "Berlin" },
1124 rawOutput: {
1125 _meta: {
1126 source: { name: "Weather", logoUrl: "https://example.com/weather.png" },
1127 },
1128 content: [{ type: "text", text: "Sunny" }],
1129 },
1130 _meta: { is_mcp_tool_call: true },
1131 },
1078 {1132 {
1079 sessionUpdate: "agent_message_chunk",1133 sessionUpdate: "agent_message_chunk",
1080 content: { type: "text", text: " → decider → event)." },1134 content: { type: "text", text: " → decider → event)." },
...@@ -1192,6 +1246,37 @@ describe("AcpAdapterV2", () => {...@@ -1192,6 +1246,37 @@ describe("AcpAdapterV2", () => {
1192 );1246 );
1193 assert.deepEqual([...childMessages.values()], ["Checking the code.", "ONE"]);1247 assert.deepEqual([...childMessages.values()], ["Checking the code.", "ONE"]);
1194 assert.equal(task?.prompt, "Run pwd, then reply ONE.");1248 assert.equal(task?.prompt, "Run pwd, then reply ONE.");
1249 // Terminal-fallback MCP calls in a child session keep their T3 identity.
1250 assert.isTrue(
1251 items.some(
1252 (item) =>
1253 item.threadId === task?.childThreadId &&
1254 item.type === "dynamic_tool" &&
1255 item.toolName === "t3-code.task_status",
1256 ),
1257 );
1258 const childMcp = items.find(
1259 (item) =>
1260 item.threadId === task?.childThreadId &&
1261 item.type === "dynamic_tool" &&
1262 item.toolName === "weather.get_weather",
1263 );
1264 const parentMcp = items.find(
1265 (item) =>
1266 item.threadId === threadId &&
1267 item.type === "dynamic_tool" &&
1268 item.toolName === "weather.get_weather",
1269 );
1270 for (const item of [parentMcp, childMcp]) {
1271 assert.equal(item?.title, "get weather");
1272 assert.deepEqual(item?.toolSource, {
1273 key: "mcp:weather",
1274 name: "Weather",
1275 kind: "integration",
1276 icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" },
1277 });
1278 assert.deepEqual(item?.toolIcon, item?.toolSource?.icon);
1279 }
1195 assert.isTrue(1280 assert.isTrue(
1196 items.some(1281 items.some(
1197 (item) =>1282 (item) =>
...@@ -1216,7 +1301,7 @@ describe("AcpAdapterV2", () => {...@@ -1216,7 +1301,7 @@ describe("AcpAdapterV2", () => {
1216 const parentTools = items.filter(1301 const parentTools = items.filter(
1217 (item) => item.threadId === threadId && item.type === "dynamic_tool",1302 (item) => item.threadId === threadId && item.type === "dynamic_tool",
1218 );1303 );
1219 assert.equal(parentTools.length, 1);1304 assert.equal(parentTools.length, 2);
1220 assert.equal(parentTools[0]?.title, "Parent tool finished");1305 assert.equal(parentTools[0]?.title, "Parent tool finished");
1221 }).pipe(Effect.provide(testLayer), Effect.scoped),1306 }).pipe(Effect.provide(testLayer), Effect.scoped),
1222 );1307 );
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts+56-15
...@@ -63,6 +63,7 @@ import {...@@ -63,6 +63,7 @@ import {
63 makeAcpMcpOverAcpBridge,63 makeAcpMcpOverAcpBridge,
64 type AcpMcpOverAcpBridge,64 type AcpMcpOverAcpBridge,
65} from "../../mcp/AcpMcpOverAcpBridge.ts";65} from "../../mcp/AcpMcpOverAcpBridge.ts";
66import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts";
66import * as McpProviderSession from "../../mcp/McpProviderSession.ts";67import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
67import {68import {
68 applyAcpAgentTerminalUpdate,69 applyAcpAgentTerminalUpdate,
...@@ -1632,6 +1633,21 @@ export function makeAcpAdapterV2(...@@ -1632,6 +1633,21 @@ export function makeAcpAdapterV2(
1632 embeddedTerminalsByToolCallId.delete(oldest);1633 embeddedTerminalsByToolCallId.delete(oldest);
1633 }1634 }
1634 };1635 };
1636 // Command lines of the terminals embedded in a tool call, so MCP calls
1637 // made through the acp-mcp-call terminal fallback keep their identity.
1638 const embeddedTerminalCommands = (
1639 sessionId: string,
1640 toolCallId: string,
1641 ): ReadonlyArray<string> =>
1642 (
1643 embeddedTerminalsByToolCallId.get(sessionScopedId(sessionId, toolCallId))
1644 ?.terminalIds ?? []
1645 ).flatMap((terminalId) => {
1646 const command =
1647 clientTerminals?.readCommandLine(terminalId) ??
1648 agentTerminalsById.get(sessionScopedId(sessionId, terminalId))?.command;
1649 return command === undefined ? [] : [command];
1650 });
1635 // Client terminals (Devin) run with the T3 server's privileges, so they1651 // Client terminals (Devin) run with the T3 server's privileges, so they
1636 // are policy-checked against the active turn policy; a command the user1652 // are policy-checked against the active turn policy; a command the user
1637 // already approved satisfies an "ask" disposition.1653 // already approved satisfies an "ask" disposition.
...@@ -3267,17 +3283,10 @@ export function makeAcpAdapterV2(...@@ -3267,17 +3283,10 @@ export function makeAcpAdapterV2(
3267 // agent-specific shape and project the same branded dynamic_tool3283 // agent-specific shape and project the same branded dynamic_tool
3268 // item native providers produce (e.g. the T3 orchestration tools).3284 // item native providers produce (e.g. the T3 orchestration tools).
3269 const mcpIdentity = extractMcpToolCallIdentity(toolCall, {3285 const mcpIdentity = extractMcpToolCallIdentity(toolCall, {
3270 embeddedTerminalCommands: (3286 embeddedTerminalCommands: embeddedTerminalCommands(
3271 embeddedTerminalsByToolCallId.get(3287 context.nativeThreadId,
3272 sessionScopedId(context.nativeThreadId, toolCall.toolCallId),3288 toolCall.toolCallId,
3273 )?.terminalIds ?? []3289 ),
3274 ).flatMap((terminalId) => {
3275 const command =
3276 clientTerminals?.readCommandLine(terminalId) ??
3277 agentTerminalsById.get(sessionScopedId(context.nativeThreadId, terminalId))
3278 ?.command;
3279 return command === undefined ? [] : [command];
3280 }),
3281 });3290 });
3282 let turnItem: OrchestrationV2TurnItem;3291 let turnItem: OrchestrationV2TurnItem;
3283 if (toolCall.toolCallId.startsWith("acp-compaction:")) {3292 if (toolCall.toolCallId.startsWith("acp-compaction:")) {
...@@ -3302,9 +3311,14 @@ export function makeAcpAdapterV2(...@@ -3302,9 +3311,14 @@ export function makeAcpAdapterV2(
3302 } else if (mcpIdentity !== undefined) {3311 } else if (mcpIdentity !== undefined) {
3303 turnItem = {3312 turnItem = {
3304 ...base,3313 ...base,
3305 // Identity lives in toolName, like native Codex MCP items; the
3306 // agent's own title (e.g. "Ran command") would shadow it.
3307 title: null,3314 title: null,
3315 ...mcpToolPresentation({
3316 serverName: mcpIdentity.server,
3317 toolName: mcpIdentity.tool,
3318 source: unknownRecord(
3319 (unknownRecord(rawOutputRecord?.result) ?? rawOutputRecord)?._meta,
3320 )?.source,
3321 }),
3308 type: "dynamic_tool",3322 type: "dynamic_tool",
3309 toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`,3323 toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`,
3310 input:3324 input:
...@@ -4328,7 +4342,8 @@ export function makeAcpAdapterV2(...@@ -4328,7 +4342,8 @@ export function makeAcpAdapterV2(
4328 return;4342 return;
4329 }4343 }
4330 if (context.finalized) return;4344 if (context.finalized) return;
4331 if (notification.sessionId !== (yield* Ref.get(activeSessionId))) {4345 const rootSessionId = yield* Ref.get(activeSessionId);
4346 if (notification.sessionId !== rootSessionId) {
4332 // Finalize may have completed during the activeSessionId yield.4347 // Finalize may have completed during the activeSessionId yield.
4333 if (context.finalized) return;4348 if (context.finalized) return;
4334 if (flavor.extractSubagentUpdate === undefined) return;4349 if (flavor.extractSubagentUpdate === undefined) return;
...@@ -4356,6 +4371,17 @@ export function makeAcpAdapterV2(...@@ -4356,6 +4371,17 @@ export function makeAcpAdapterV2(
4356 const key = `${nativeTaskId}:tool:${toolCall.toolCallId}`;4371 const key = `${nativeTaskId}:tool:${toolCall.toolCallId}`;
4357 const merged = mergeToolCallState(context.tools.get(key), toolCall);4372 const merged = mergeToolCallState(context.tools.get(key), toolCall);
4358 context.tools.set(key, merged);4373 context.tools.set(key, merged);
4374 // Terminals are remembered under the raw session id: the child's
4375 // own session, or the root one when the flavor routes child
4376 // updates out of it (Devin).
4377 const mcpIdentity = extractMcpToolCallIdentity(merged, {
4378 embeddedTerminalCommands: [
4379 ...embeddedTerminalCommands(notification.sessionId, toolCall.toolCallId),
4380 ...(rootSessionId === null
4381 ? []
4382 : embeddedTerminalCommands(rootSessionId, toolCall.toolCallId)),
4383 ],
4384 });
4359 const now = yield* DateTime.now;4385 const now = yield* DateTime.now;
4360 const status = toolStatus(merged.status);4386 const status = toolStatus(merged.status);
4361 const startedAt = context.toolStartedAt.get(key) ?? now;4387 const startedAt = context.toolStartedAt.get(key) ?? now;
...@@ -4380,7 +4406,22 @@ export function makeAcpAdapterV2(...@@ -4380,7 +4406,22 @@ export function makeAcpAdapterV2(
4380 completedAt: completedAtForStatus(status, now),4406 completedAt: completedAtForStatus(status, now),
4381 updatedAt: now,4407 updatedAt: now,
4382 type: "dynamic_tool",4408 type: "dynamic_tool",
4383 toolName: merged.title ?? merged.kind ?? "Tool",4409 ...(mcpIdentity === undefined
4410 ? {}
4411 : mcpToolPresentation({
4412 serverName: mcpIdentity.server,
4413 toolName: mcpIdentity.tool,
4414 source: unknownRecord(
4415 (
4416 unknownRecord(unknownRecord(merged.data.rawOutput)?.result) ??
4417 unknownRecord(merged.data.rawOutput)
4418 )?._meta,
4419 )?.source,
4420 })),
4421 toolName:
4422 mcpIdentity === undefined
4423 ? (merged.title ?? merged.kind ?? "Tool")
4424 : `${mcpIdentity.server}.${mcpIdentity.tool}`,
4384 input: merged.data.rawInput ?? null,4425 input: merged.data.rawInput ?? null,
4385 output: merged.data.rawOutput ?? merged.data.content ?? null,4426 output: merged.data.rawOutput ?? merged.data.content ?? null,
4386 },4427 },
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+105
...@@ -2195,6 +2195,111 @@ describe("ClaudeAdapterV2 background wake turns", () => {...@@ -2195,6 +2195,111 @@ describe("ClaudeAdapterV2 background wake turns", () => {
2195 });2195 });
2196 const makeWakeHarness = makeWakeHarnessWithOptions();2196 const makeWakeHarness = makeWakeHarnessWithOptions();
21972197
2198 it.effect.each([
2199 { isError: false, title: "Check weather" },
2200 { isError: true, title: "Check weather" },
2201 { isError: false, title: undefined },
2202 ])("keeps late MCP display metadata with %j", ({ isError, title }) =>
2203 Effect.gen(function* () {
2204 const harness = yield* makeWakeHarness;
2205 yield* harness.runtime.startTurn(
2206 makeClaudeTestTurnInput({
2207 threadId: harness.threadId,
2208 providerThread: harness.providerThread,
2209 now: yield* DateTime.now,
2210 attemptId: RunAttemptId.make("mcp-presentation"),
2211 text: "Check the weather",
2212 attachments: [],
2213 }),
2214 );
2215 const toolName = "mcp__weather__get_weather";
2216 const id = "weather-call";
2217 yield* Effect.promise(() =>
2218 harness.getOpenedOptions()!.canUseTool!(
2219 toolName,
2220 { city: "Berlin" },
2221 {
2222 signal: new AbortController().signal,
2223 toolUseID: id,
2224 requestId: "weather-request",
2225 },
2226 ),
2227 );
2228 yield* harness.offerAndWait(
2229 claudeSdkFrame({
2230 type: "assistant",
2231 uuid: "weather-assistant",
2232 session_id: WAKE_NATIVE_SESSION,
2233 parent_tool_use_id: null,
2234 message: {
2235 id: "weather-message",
2236 type: "message",
2237 role: "assistant",
2238 model: "claude-sonnet-4-6",
2239 content: [{ type: "tool_use", id, name: toolName, input: { city: "Berlin" } }],
2240 },
2241 tool_use_meta: [
2242 {
2243 id,
2244 display_name: title,
2245 server_display_name: "Weather",
2246 icon_url: "https://example.com/weather.png",
2247 },
2248 ],
2249 }),
2250 );
2251 yield* harness.offerAndWait(
2252 claudeSdkFrame({
2253 type: "user",
2254 uuid: "weather-result",
2255 session_id: WAKE_NATIVE_SESSION,
2256 parent_tool_use_id: null,
2257 message: {
2258 role: "user",
2259 content: [
2260 {
2261 type: "tool_result",
2262 tool_use_id: id,
2263 content: "Weather result",
2264 is_error: isError,
2265 },
2266 ],
2267 },
2268 }),
2269 );
2270 yield* Queue.offer(
2271 harness.sdkMessages,
2272 makeResultFrame({ uuid: "weather-terminal", result: "Weather checked" }),
2273 );
2274 yield* Queue.take(harness.terminalReceipts);
2275 const items = harness.events.flatMap((event) =>
2276 event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool"
2277 ? [event.turnItem]
2278 : [],
2279 );
2280 assert.deepEqual(
2281 items.map((item) => item.status),
2282 ["running", "running", isError ? "failed" : "completed"],
2283 );
2284 assert.isNull(items[0]?.title);
2285 for (const item of items.slice(1)) {
2286 assert.equal(item.title, title ?? "get weather");
2287 assert.deepEqual(item.toolIcon, {
2288 _tag: "themed-logo",
2289 logoUrl: "https://example.com/weather.png",
2290 });
2291 assert.deepEqual(item.toolSource, {
2292 key: "mcp:weather",
2293 name: "Weather",
2294 kind: "integration",
2295 icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" },
2296 });
2297 assert.deepEqual(item.input, { city: "Berlin" });
2298 }
2299 assert.equal(new Set(items.map((item) => item.id)).size, 1);
2300 }).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
2301 );
2302
2198 it.effect(2303 it.effect(
2199 "reuses a background shell's query for omitted and explicit Normal, but blocks Fast",2304 "reuses a background shell's query for omitted and explicit Normal, but blocks Fast",
2200 () =>2305 () =>
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+27-49
...@@ -67,7 +67,6 @@ import {...@@ -67,7 +67,6 @@ import {
67 type ProviderUserInputAnswers,67 type ProviderUserInputAnswers,
68 type ProviderThreadId,68 type ProviderThreadId,
69 type ThreadId,69 type ThreadId,
70 type ToolActivitySource,
71} from "@t3tools/contracts";70} from "@t3tools/contracts";
7271
73import * as Cause from "effect/Cause";72import * as Cause from "effect/Cause";
...@@ -116,6 +115,7 @@ import type { ServerProviderShape } from "../../provider/Services/ServerProvider...@@ -116,6 +115,7 @@ import type { ServerProviderShape } from "../../provider/Services/ServerProvider
116import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts";115import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts";
117import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts";116import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts";
118import { buildRuntimeInstructions } from "../../provider/RuntimeInstructions.ts";117import { buildRuntimeInstructions } from "../../provider/RuntimeInstructions.ts";
118import { mcpToolPresentation, normalizeMcpText } from "../../provider/McpToolPresentation.ts";
119import * as McpProviderSession from "../../mcp/McpProviderSession.ts";119import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
120import * as IdAllocator from "../IdAllocator.ts";120import * as IdAllocator from "../IdAllocator.ts";
121import { makeProviderFailure, makeProviderRetryTurnItem } from "../ProviderFailure.ts";121import { makeProviderFailure, makeProviderRetryTurnItem } from "../ProviderFailure.ts";
...@@ -2089,62 +2089,33 @@ function claudeToolUseBlocksFromAssistantMessage(...@@ -2089,62 +2089,33 @@ function claudeToolUseBlocksFromAssistantMessage(
2089 return message.message.content.filter(isClaudeToolUseContentBlock);2089 return message.message.content.filter(isClaudeToolUseContentBlock);
2090}2090}
20912091
2092interface ClaudeToolPresentation {2092type ClaudeToolPresentation = ReturnType<typeof mcpToolPresentation>;
2093 readonly title: string;
2094 readonly toolSource?: ToolActivitySource;
2095}
2096
2097function boundedMetaText(value: unknown, maxLength: number): string | undefined {
2098 if (typeof value !== "string") return undefined;
2099 const text = value.trim().replace(/\s+/gu, " ");
2100 return text.length > 0 && text.length <= maxLength ? text : undefined;
2101}
2102
2103function metaHttpUrl(value: unknown): string | undefined {
2104 if (typeof value !== "string" || value.length > 4096) return undefined;
2105 try {
2106 const url = new URL(value);
2107 return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096
2108 ? url.href
2109 : undefined;
2110 } catch {
2111 return undefined;
2112 }
2113}
21142093
2115/**
2116 * Claude Code's own label for each tool use (the MCP tool's title, or its
2117 * humanized name) plus the server's name and icon. The CLI sends these as
2118 * `tool_use_meta` on assistant frames; the SDK types do not declare it yet.
2119 */
2120function claudeToolPresentationsFromAssistantMessage(2094function claudeToolPresentationsFromAssistantMessage(
2121 message: SDKMessage,2095 message: SDKMessage,
2122): ReadonlyMap<string, ClaudeToolPresentation> {2096): ReadonlyMap<string, ClaudeToolPresentation> {
2123 const presentations = new Map<string, ClaudeToolPresentation>();2097 const presentations = new Map<string, ClaudeToolPresentation>();
2124 const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined;2098 const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined;
2125 if (!Array.isArray(meta)) return presentations;2099 if (!Array.isArray(meta)) return presentations;
2100 const toolNames = new Map(
2101 claudeToolUseBlocksFromAssistantMessage(message).map((tool) => [
2102 tool.id,
2103 tool.name.replace(/^mcp__claude_ai_/u, "mcp__"),
2104 ]),
2105 );
2126 for (const entry of meta) {2106 for (const entry of meta) {
2127 if (typeof entry !== "object" || entry === null) continue;2107 if (typeof entry !== "object" || entry === null) continue;
2128 const id = boundedMetaText(Reflect.get(entry, "id"), 512);2108 const id = normalizeMcpText(Reflect.get(entry, "id"), 512);
2129 const title = boundedMetaText(Reflect.get(entry, "display_name"), 160);2109 if (id === undefined) continue;
2130 if (id === undefined || title === undefined) continue;2110 presentations.set(
2131 const serverName = boundedMetaText(Reflect.get(entry, "server_display_name"), 160);2111 id,
2132 const iconUrl = metaHttpUrl(Reflect.get(entry, "icon_url"));2112 mcpToolPresentation({
2133 presentations.set(id, {2113 toolName: toolNames.get(id),
2134 title,2114 title: Reflect.get(entry, "display_name"),
2135 ...(serverName === undefined2115 serverDisplayName: Reflect.get(entry, "server_display_name"),
2136 ? {}2116 iconUrl: Reflect.get(entry, "icon_url"),
2137 : {2117 }),
2138 toolSource: {2118 );
2139 key: `mcp:${serverName.toLowerCase()}`,
2140 name: serverName,
2141 kind: "integration",
2142 ...(iconUrl === undefined
2143 ? {}
2144 : { icon: { _tag: "themed-logo", logoUrl: iconUrl } as const }),
2145 },
2146 }),
2147 });
2148 }2119 }
2149 return presentations;2120 return presentations;
2150}2121}
...@@ -3878,6 +3849,9 @@ export function makeClaudeAdapterV2(...@@ -3878,6 +3849,9 @@ export function makeClaudeAdapterV2(
3878 : {3849 : {
3879 ...itemBase,3850 ...itemBase,
3880 type: "dynamic_tool",3851 type: "dynamic_tool",
3852 ...(input.presentation?.toolIcon === undefined
3853 ? {}
3854 : { toolIcon: input.presentation.toolIcon }),
3881 ...(input.presentation?.toolSource === undefined3855 ...(input.presentation?.toolSource === undefined
3882 ? {}3856 ? {}
3883 : { toolSource: input.presentation.toolSource }),3857 : { toolSource: input.presentation.toolSource }),
...@@ -6053,7 +6027,11 @@ export function makeClaudeAdapterV2(...@@ -6053,7 +6027,11 @@ export function makeClaudeAdapterV2(
6053 toolName: toolUse.name,6027 toolName: toolUse.name,
6054 toolInput: nativeToolInput,6028 toolInput: nativeToolInput,
6055 parentToolUseId: parentToolUseIdFromSdkMessage(message),6029 parentToolUseId: parentToolUseIdFromSdkMessage(message),
6056 presentation: toolPresentations.get(toolUse.id),6030 presentation:
6031 toolPresentations.get(toolUse.id) ??
6032 mcpToolPresentation({
6033 toolName: toolUse.name.replace(/^mcp__claude_ai_/u, "mcp__"),
6034 }),
6057 });6035 });
6058 const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id);6036 const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id);
6059 if (heldPlan !== undefined) {6037 if (heldPlan !== undefined) {
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+50-2
...@@ -823,14 +823,62 @@ describe("CodexAdapterV2 dynamic tool projection", () => {...@@ -823,14 +823,62 @@ describe("CodexAdapterV2 dynamic tool projection", () => {
823 );823 );
824 assert.equal(824 assert.equal(
825 CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title,825 CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title,
826 undefined,826 "js",
827 );827 );
828 assert.equal(828 assert.equal(
829 CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" }).title,829 CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" }).title,
830 undefined,830 "js",
831 );831 );
832 });832 });
833833
834 it.each(["inProgress", "completed", "failed"] as const)(
835 "presents ordinary MCP calls when %s",
836 (status) => {
837 const projection = CodexAdapterV2.projectCodexDynamicToolItem({
838 type: "mcpToolCall",
839 id: "weather-call",
840 server: "weather",
841 tool: "get_weather",
842 status,
843 arguments: { city: "Berlin" },
844 });
845 assert.equal(projection.title, "get weather");
846 assert.deepEqual(projection.toolSource, {
847 key: "mcp:weather",
848 name: "weather",
849 kind: "integration",
850 });
851 assert.deepEqual(projection.input, { city: "Berlin" });
852 },
853 );
854
855 it("uses Codex connector names without reading a display title from arguments", () => {
856 const projection = CodexAdapterV2.projectCodexDynamicToolItem({
857 type: "mcpToolCall",
858 id: "connector-call",
859 server: "_apps",
860 tool: "connector_get_weather",
861 status: "completed",
862 arguments: { title: "Argument, not display metadata" },
863 appContext: {
864 connectorId: "weather-app",
865 appName: "Weather",
866 actionName: "Check weather",
867 },
868 result: {
869 content: [],
870 _meta: { source: { logoUrl: "https://example.com/weather.png" } },
871 },
872 });
873 assert.equal(projection.title, "Check weather");
874 assert.equal(projection.toolSource?.name, "Weather");
875 assert.deepEqual(projection.toolIcon, {
876 _tag: "themed-logo",
877 logoUrl: "https://example.com/weather.png",
878 });
879 assert.deepEqual(projection.toolSource?.icon, projection.toolIcon);
880 });
881
834 it("preserves native browser and app icons alongside MCP tool output", () => {882 it("preserves native browser and app icons alongside MCP tool output", () => {
835 const browser = CodexAdapterV2.projectCodexDynamicToolItem({883 const browser = CodexAdapterV2.projectCodexDynamicToolItem({
836 type: "mcpToolCall",884 type: "mcpToolCall",
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+3-2
...@@ -489,9 +489,10 @@ export function projectCodexDynamicToolItem(...@@ -489,9 +489,10 @@ export function projectCodexDynamicToolItem(
489 item.type === "mcpToolCall"489 item.type === "mcpToolCall"
490 ? `${item.server}.${item.tool}`490 ? `${item.server}.${item.tool}`
491 : [trimText(item.namespace), item.tool].filter(Boolean).join(".");491 : [trimText(item.namespace), item.tool].filter(Boolean).join(".");
492 const title = dynamicToolTitle(toolName, item.arguments);492 const presentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {};
493 const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title;
493 const projection: CodexDynamicToolProjection = {494 const projection: CodexDynamicToolProjection = {
494 ...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}),495 ...presentation,
495 toolName,496 toolName,
496 ...(title ? { title } : {}),497 ...(title ? { title } : {}),
497 input: item.arguments,498 input: item.arguments,
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts+40
...@@ -359,6 +359,22 @@ describe("CursorAdapterV2", () => {...@@ -359,6 +359,22 @@ describe("CursorAdapterV2", () => {
359 numFiles: 0,359 numFiles: 0,
360 };360 };
361 const updates: ReadonlyArray<InteractionUpdate> = [361 const updates: ReadonlyArray<InteractionUpdate> = [
362 ...(["tool-call-started", "tool-call-completed"] as const).map((type) => ({
363 type,
364 modelCallId: "native-model-call",
365 callId: "mcp-weather",
366 toolCall: {
367 type: "mcp" as const,
368 args: {
369 providerIdentifier: "weather",
370 toolName: "get_weather",
371 args: { city: "Berlin" },
372 },
373 ...(type === "tool-call-completed"
374 ? { result: { status: "success" as const, value: { content: [], isError: false } } }
375 : {}),
376 },
377 })),
362 {378 {
363 type: "tool-call-completed",379 type: "tool-call-completed",
364 modelCallId: "native-model-call",380 modelCallId: "native-model-call",
...@@ -625,6 +641,30 @@ describe("CursorAdapterV2", () => {...@@ -625,6 +641,30 @@ describe("CursorAdapterV2", () => {
625 Stream.takeUntil((event) => event.type === "turn.terminal"),641 Stream.takeUntil((event) => event.type === "turn.terminal"),
626 Stream.runCollect,642 Stream.runCollect,
627 );643 );
644 const mcpItems = events.flatMap((event) =>
645 event.type === "turn_item.updated" &&
646 event.turnItem.type === "dynamic_tool" &&
647 event.turnItem.toolName === "mcp__weather__get_weather"
648 ? [event.turnItem]
649 : [],
650 );
651 assert.deepEqual(
652 mcpItems.map((item) => item.status),
653 ["running", "completed"],
654 );
655 for (const item of mcpItems) {
656 assert.equal(item.title, "get weather");
657 assert.deepEqual(item.toolSource, {
658 key: "mcp:weather",
659 name: "weather",
660 kind: "integration",
661 });
662 assert.deepEqual(item.input, {
663 providerIdentifier: "weather",
664 toolName: "get_weather",
665 args: { city: "Berlin" },
666 });
667 }
628 const fileSearchItems = events.flatMap((event) =>668 const fileSearchItems = events.flatMap((event) =>
629 event.type === "turn_item.updated" &&669 event.type === "turn_item.updated" &&
630 event.turnItem.type === "file_search" &&670 event.turnItem.type === "file_search" &&
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts+7
...@@ -44,6 +44,7 @@ import * as Stream from "effect/Stream";...@@ -44,6 +44,7 @@ import * as Stream from "effect/Stream";
4444
45import { resolveAttachmentPath } from "../../attachmentStore.ts";45import { resolveAttachmentPath } from "../../attachmentStore.ts";
46import * as ServerConfig from "../../config.ts";46import * as ServerConfig from "../../config.ts";
47import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts";
47import * as McpProviderSession from "../../mcp/McpProviderSession.ts";48import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
48import { CursorTransportFailure } from "../../provider/acp/CursorTransportFailure.ts";49import { CursorTransportFailure } from "../../provider/acp/CursorTransportFailure.ts";
49import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts";50import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts";
...@@ -1282,6 +1283,12 @@ export function makeCursorAdapterV2(...@@ -1282,6 +1283,12 @@ export function makeCursorAdapterV2(
1282 turnItem = {1283 turnItem = {
1283 ...base,1284 ...base,
1284 type: "dynamic_tool",1285 type: "dynamic_tool",
1286 ...(toolCall.type === "mcp"
1287 ? mcpToolPresentation({
1288 serverName: toolCall.args.providerIdentifier,
1289 toolName: toolCall.args.toolName,
1290 })
1291 : {}),
1285 toolName: cursorToolName(toolCall),1292 toolName: cursorToolName(toolCall),
1286 input: toolCall.args,1293 input: toolCall.args,
1287 ...(cursorToolOutput(toolCall) === undefined1294 ...(cursorToolOutput(toolCall) === undefined
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts+244
...@@ -902,6 +902,250 @@ describe("OpenCodeAdapterV2", () => {...@@ -902,6 +902,250 @@ describe("OpenCodeAdapterV2", () => {
902 }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped),902 }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped),
903 );903 );
904904
905 it.effect("presents OpenCode MCP calls without treating remote tools as local edits", () =>
906 Effect.gen(function* () {
907 const nativeEvents = asyncEventStream();
908 const nativeSessionId = "native-opencode-mcp";
909 let statusReads = 0;
910 const harness = yield* makeOpenCodeRuntimeHarness("mcp-presentation", nativeSessionId, {
911 event: {
912 subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => {
913 options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true });
914 return { stream: nativeEvents.stream };
915 },
916 },
917 session: {
918 create: async () => ({ data: { id: nativeSessionId, time: { created: 1, updated: 1 } } }),
919 promptAsync: async () => ({ data: true }),
920 },
921 mcp: {
922 status: async () => {
923 statusReads++;
924 return {
925 data: {
926 "my.server_with_underscores": { status: "connected" },
927 ambiguous: { status: "connected" },
928 ambiguous_server: { status: "connected" },
929 code: { status: "connected" },
930 apply: { status: "connected" },
931 },
932 };
933 },
934 },
935 });
936 yield* harness.startTurn();
937 const received = yield* harness.runtime.events.pipe(
938 Stream.takeUntil(
939 (event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction",
940 ),
941 Stream.runCollect,
942 Effect.forkScoped,
943 );
944 for (const status of ["running", "completed", "error"] as const) {
945 yield* Effect.promise(() =>
946 nativeEvents.push({
947 type: "message.part.updated",
948 properties: {
949 sessionID: nativeSessionId,
950 part: {
951 id: "mcp-part",
952 sessionID: nativeSessionId,
953 messageID: "mcp-assistant",
954 type: "tool",
955 callID: "mcp-call",
956 tool: "my_server_with_underscores_edit_document",
957 state: {
958 status,
959 input: { document: "remote-doc" },
960 title: "Edit remote document",
961 metadata: {},
962 output: "saved",
963 error: "failed",
964 time: { start: 1, end: 2 },
965 },
966 },
967 },
968 }),
969 );
970 }
971 yield* Effect.promise(() =>
972 nativeEvents.push({
973 type: "message.part.updated",
974 properties: {
975 sessionID: nativeSessionId,
976 part: {
977 id: "ambiguous-part",
978 sessionID: nativeSessionId,
979 messageID: "mcp-assistant",
980 type: "tool",
981 callID: "ambiguous-call",
982 tool: "ambiguous_server_edit_document",
983 state: {
984 status: "completed",
985 input: {},
986 title: "Ambiguous tool",
987 metadata: {},
988 output: "",
989 time: { start: 1, end: 2 },
990 },
991 },
992 },
993 }),
994 );
995 for (const tool of ["code_search", "apply_patch"]) {
996 yield* Effect.promise(() =>
997 nativeEvents.push({
998 type: "message.part.updated",
999 properties: {
1000 sessionID: nativeSessionId,
1001 part: {
1002 id: `native-${tool}`,
1003 sessionID: nativeSessionId,
1004 messageID: "mcp-assistant",
1005 type: "tool",
1006 callID: `native-${tool}`,
1007 tool,
1008 state: {
1009 status: "completed",
1010 input: { query: "needle", filePath: "local.ts" },
1011 title: `Native ${tool}`,
1012 metadata: {},
1013 output: "done",
1014 time: { start: 1, end: 2 },
1015 },
1016 },
1017 },
1018 }),
1019 );
1020 }
1021 yield* Effect.promise(() =>
1022 nativeEvents.push({
1023 type: "session.compacted",
1024 properties: { sessionID: nativeSessionId },
1025 }),
1026 );
1027 const allItems = (yield* Fiber.join(received)).flatMap((event) =>
1028 event.type === "turn_item.updated" ? [event.turnItem] : [],
1029 );
1030 assert.equal(
1031 allItems.find((item) => item.title === "Native code_search")?.type,
1032 "web_search",
1033 );
1034 assert.equal(
1035 allItems.find((item) => item.title === "Native apply_patch")?.type,
1036 "file_change",
1037 );
1038 const items = allItems.filter((item) => item.type === "dynamic_tool");
1039 assert.deepEqual(
1040 items.slice(0, 3).map((item) => item.status),
1041 ["running", "completed", "failed"],
1042 );
1043 assert.deepEqual(
1044 items.slice(0, 3).map((item) => item.title),
1045 ["Edit remote document", "Edit remote document", "edit document"],
1046 );
1047 for (const item of items.slice(0, 3)) {
1048 assert.deepEqual(item.toolSource, {
1049 key: "mcp:my.server_with_underscores",
1050 name: "my.server with underscores",
1051 kind: "integration",
1052 });
1053 assert.deepEqual(item.input, { document: "remote-doc" });
1054 }
1055 assert.equal(items[3]?.toolName, "ambiguous_server_edit_document");
1056 assert.isUndefined(items[3]?.toolSource);
1057 assert.equal(statusReads, 1);
1058 }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped),
1059 );
1060
1061 it.effect.each(["failure", "timeout"] as const)("retries MCP status after %s", (failure) =>
1062 Effect.gen(function* () {
1063 const nativeEvents = asyncEventStream();
1064 const called = promiseGate<void>();
1065 const sessionId = `mcp-status-${failure}`;
1066 let statusReads = 0;
1067 let signal: AbortSignal | undefined;
1068 const harness = yield* makeOpenCodeRuntimeHarness(sessionId, sessionId, {
1069 event: { subscribe: async () => ({ stream: nativeEvents.stream }) },
1070 session: {
1071 create: async () => ({ data: { id: sessionId, time: { created: 1, updated: 1 } } }),
1072 promptAsync: async () => ({ data: true }),
1073 },
1074 mcp: {
1075 status: async (_input: unknown, options: { signal?: AbortSignal }) => {
1076 statusReads++;
1077 if (statusReads === 1) {
1078 signal = options?.signal;
1079 called.resolve();
1080 if (failure === "timeout") return new Promise(() => {});
1081 throw new Error("MCP status unavailable");
1082 }
1083 return { data: { weather: { status: "connected" } } };
1084 },
1085 },
1086 });
1087 yield* harness.startTurn();
1088 const received = yield* harness.runtime.events.pipe(
1089 Stream.takeUntil(
1090 (event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction",
1091 ),
1092 Stream.runCollect,
1093 Effect.forkScoped,
1094 );
1095 for (const status of ["running", "completed"] as const) {
1096 const emitted = yield* Effect.promise(() =>
1097 nativeEvents.push({
1098 type: "message.part.updated",
1099 properties: {
1100 sessionID: sessionId,
1101 part: {
1102 id: "weather-part",
1103 sessionID: sessionId,
1104 messageID: "weather-message",
1105 type: "tool",
1106 callID: "weather-call",
1107 tool: "weather_edit_document",
1108 state: {
1109 status,
1110 input: { document: "remote-doc" },
1111 title: "Edit remote document",
1112 metadata: {},
1113 output: "saved",
1114 time: { start: 1, end: 2 },
1115 },
1116 },
1117 },
1118 }),
1119 ).pipe(Effect.forkScoped);
1120 if (status === "running" && failure === "timeout") {
1121 yield* Effect.promise(() => called.promise);
1122 yield* TestClock.adjust("1 second");
1123 }
1124 yield* Fiber.join(emitted);
1125 }
1126 yield* Effect.promise(() =>
1127 nativeEvents.push({ type: "session.compacted", properties: { sessionID: sessionId } }),
1128 );
1129 const completed = (yield* Fiber.join(received)).find(
1130 (event) =>
1131 event.type === "turn_item.updated" &&
1132 event.turnItem.type === "dynamic_tool" &&
1133 event.turnItem.status === "completed",
1134 );
1135 assert.equal(statusReads, 2);
1136 assert.equal(
1137 completed?.type === "turn_item.updated" && completed.turnItem.title,
1138 "Edit remote document",
1139 );
1140 assert.deepEqual(completed?.type === "turn_item.updated" && completed.turnItem.toolSource, {
1141 key: "mcp:weather",
1142 name: "weather",
1143 kind: "integration",
1144 });
1145 if (failure === "timeout") assert.isTrue(signal?.aborted);
1146 }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped),
1147 );
1148
905 it.effect("admits a native command on its user receipt before generation completes", () =>1149 it.effect("admits a native command on its user receipt before generation completes", () =>
906 Effect.gen(function* () {1150 Effect.gen(function* () {
907 const nativeEvents = asyncEventStream();1151 const nativeEvents = asyncEventStream();
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts+1
...@@ -383,6 +383,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient...@@ -383,6 +383,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient
383 reply: (input: unknown) => request("question.reply", input),383 reply: (input: unknown) => request("question.reply", input),
384 },384 },
385 mcp: {385 mcp: {
386 status: () => request("mcp.status", {}),
386 add: (input: unknown) => request("mcp.add", input),387 add: (input: unknown) => request("mcp.add", input),
387 },388 },
388 } as unknown as OpencodeClient;389 } as unknown as OpencodeClient;
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts+44-6
...@@ -56,6 +56,7 @@ import * as Stream from "effect/Stream";...@@ -56,6 +56,7 @@ import * as Stream from "effect/Stream";
5656
57import { resolveAttachmentPath } from "../../attachmentStore.ts";57import { resolveAttachmentPath } from "../../attachmentStore.ts";
58import * as ServerConfig from "../../config.ts";58import * as ServerConfig from "../../config.ts";
59import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts";
59import * as McpProviderSession from "../../mcp/McpProviderSession.ts";60import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
60import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";61import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";
61import * as ProviderEventLoggers from "../../provider/Layers/ProviderEventLoggers.ts";62import * as ProviderEventLoggers from "../../provider/Layers/ProviderEventLoggers.ts";
...@@ -286,6 +287,7 @@ interface ActiveOpenCodeTurn {...@@ -286,6 +287,7 @@ interface ActiveOpenCodeTurn {
286 readonly parts: Map<string, Exclude<OpenCodePart, ToolPart>>;287 readonly parts: Map<string, Exclude<OpenCodePart, ToolPart>>;
287 readonly partIdsByMessage: Map<string, Set<string>>;288 readonly partIdsByMessage: Map<string, Set<string>>;
288 readonly toolNamesByCallId: Map<string, string>;289 readonly toolNamesByCallId: Map<string, string>;
290 mcpServerNames?: ReadonlyArray<string>;
289 readonly providerTurn: OrchestrationV2ProviderTurn;291 readonly providerTurn: OrchestrationV2ProviderTurn;
290 nextItemOrdinal: number;292 nextItemOrdinal: number;
291 nativeUserMessageId: string | null;293 nativeUserMessageId: string | null;
...@@ -1593,12 +1595,48 @@ export function makeOpenCodeAdapterV2(...@@ -1593,12 +1595,48 @@ export function makeOpenCodeAdapterV2(
1593 | "completedAt"1595 | "completedAt"
1594 | "updatedAt"1596 | "updatedAt"
1595 >;1597 >;
1596 const turnItem = openCodeToolTurnItem(base, {1598 const input = toolInput(part);
1597 name: part.tool,1599 const output = toolOutput(part);
1598 input: toolInput(part),1600 const isNativeTool = part.tool === "code_search" || part.tool === "apply_patch";
1599 output: toolOutput(part),1601 if (!isNativeTool && part.tool.includes("_") && turn.mcpServerNames === undefined) {
1600 completedMetadata: part.state.status === "completed" ? part.state.metadata : undefined,1602 const serverNames = yield* OpenCodeRuntime.runOpenCodeSdk("mcp.status", (signal) =>
1601 });1603 client.mcp.status(undefined, { signal, throwOnError: true }),
1604 ).pipe(
1605 Effect.timeout("1 second"),
1606 Effect.map((response) => Object.keys(response.data ?? {})),
1607 Effect.catch(() => Effect.succeed(undefined)),
1608 );
1609 if (serverNames !== undefined) turn.mcpServerNames = serverNames;
1610 }
1611 const matchingServers = turn.mcpServerNames?.filter(
1612 (name) =>
1613 !isNativeTool && part.tool.startsWith(`${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_`),
1614 );
1615 const serverName = matchingServers?.length === 1 ? matchingServers[0] : undefined;
1616 const presentation =
1617 serverName === undefined
1618 ? {}
1619 : mcpToolPresentation({
1620 serverName,
1621 toolName: part.tool.slice(serverName.replace(/[^a-zA-Z0-9_-]/g, "_").length + 1),
1622 title: toolTitle(part) === part.tool ? undefined : toolTitle(part),
1623 });
1624 const turnItem: OrchestrationV2TurnItem = matchingServers?.length
1625 ? {
1626 ...base,
1627 type: "dynamic_tool",
1628 ...presentation,
1629 toolName: part.tool,
1630 input,
1631 ...(output === undefined ? {} : { output }),
1632 }
1633 : openCodeToolTurnItem(base, {
1634 name: part.tool,
1635 input,
1636 output,
1637 completedMetadata:
1638 part.state.status === "completed" ? part.state.metadata : undefined,
1639 });
1602 yield* emitProviderEvent({1640 yield* emitProviderEvent({
1603 type: "node.updated",1641 type: "node.updated",
1604 driver: OPENCODE_PROVIDER,1642 driver: OPENCODE_PROVIDER,
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts+41
...@@ -1063,6 +1063,47 @@ describe("PiAdapterV2", () => {...@@ -1063,6 +1063,47 @@ describe("PiAdapterV2", () => {
1063 }).pipe(Effect.scoped, Effect.provide(testLayer)),1063 }).pipe(Effect.scoped, Effect.provide(testLayer)),
1064 );1064 );
10651065
1066 it.effect("presents explicitly namespaced MCP extension tools", () =>
1067 Effect.gen(function* () {
1068 const fake = yield* makeFakePi;
1069 const { runtime, takeEvent } = yield* openRuntime(fake);
1070 const providerThread = yield* runtime.ensureThread({
1071 threadId: THREAD_ID,
1072 modelSelection: modelSelection("default"),
1073 runtimePolicy,
1074 });
1075 yield* startTurn(runtime, providerThread);
1076 yield* fake.takeRequest("prompt");
1077 yield* fake.emit({ type: "agent_start" });
1078 for (const type of ["tool_execution_start", "tool_execution_end"]) {
1079 yield* fake.emit({
1080 type,
1081 toolCallId: "weather-call",
1082 toolName: "mcp__weather__get_weather",
1083 args: { city: "Berlin" },
1084 result: { content: [{ type: "text", text: "Sunny" }] },
1085 isError: false,
1086 });
1087 const event = yield* takeEvent(
1088 (event) => event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool",
1089 );
1090 if (event.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool")
1091 return yield* Effect.die("Expected an MCP tool item");
1092 assert.equal(event.turnItem.title, "get weather");
1093 assert.equal(
1094 event.turnItem.status,
1095 type === "tool_execution_start" ? "running" : "completed",
1096 );
1097 assert.deepEqual(event.turnItem.toolSource, {
1098 key: "mcp:weather",
1099 name: "weather",
1100 kind: "integration",
1101 });
1102 assert.deepEqual(event.turnItem.input, { city: "Berlin" });
1103 }
1104 }).pipe(Effect.scoped, Effect.provide(testLayer)),
1105 );
1106
1066 it.effect("observes official subagent results without inventing child threads", () =>1107 it.effect("observes official subagent results without inventing child threads", () =>
1067 Effect.gen(function* () {1108 Effect.gen(function* () {
1068 const fake = yield* makeFakePi;1109 const fake = yield* makeFakePi;
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts+2
...@@ -61,6 +61,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";...@@ -61,6 +61,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";
6161
62import { resolveAttachmentPath } from "../../attachmentStore.ts";62import { resolveAttachmentPath } from "../../attachmentStore.ts";
63import * as ServerConfig from "../../config.ts";63import * as ServerConfig from "../../config.ts";
64import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts";
64import * as McpProviderSession from "../../mcp/McpProviderSession.ts";65import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
65import {66import {
66 expandPiSkillReference,67 expandPiSkillReference,
...@@ -1016,6 +1017,7 @@ export function makePiAdapterV2(...@@ -1016,6 +1017,7 @@ export function makePiAdapterV2(
1016 ...shared,1017 ...shared,
1017 title: toolName,1018 title: toolName,
1018 type: "dynamic_tool",1019 type: "dynamic_tool",
1020 ...mcpToolPresentation({ toolName }),
1019 toolName,1021 toolName,
1020 input: args ?? {},1022 input: args ?? {},
1021 ...(outputText.length > 0 ? { output: outputText } : {}),1023 ...(outputText.length > 0 ? { output: outputText } : {}),
apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts+7-2
...@@ -35,6 +35,7 @@ export function assertClaudeMcpToolPresentationOutput(...@@ -35,6 +35,7 @@ export function assertClaudeMcpToolPresentationOutput(
3535
36 assert.equal(scrape?.status, "completed");36 assert.equal(scrape?.status, "completed");
37 assert.equal(scrape?.title, "Firecrawl scrape");37 assert.equal(scrape?.title, "Firecrawl scrape");
38 assert.deepEqual(scrape?.toolIcon, scrape?.toolSource?.icon);
38 assert.deepEqual(scrape?.toolSource, {39 assert.deepEqual(scrape?.toolSource, {
39 key: "mcp:firecrawl",40 key: "mcp:firecrawl",
40 name: "Firecrawl",41 name: "Firecrawl",
...@@ -46,6 +47,10 @@ export function assertClaudeMcpToolPresentationOutput(...@@ -46,6 +47,10 @@ export function assertClaudeMcpToolPresentationOutput(
46 });47 });
4748
48 assert.equal(map?.status, "completed");49 assert.equal(map?.status, "completed");
49 assert.isNull(map?.title);50 assert.equal(map?.title, "firecrawl map");
50 assert.notProperty(map, "toolSource");51 assert.deepEqual(map?.toolSource, {
52 key: "mcp:firecrawl",
53 name: "Firecrawl",
54 kind: "integration",
55 });
51}56}
apps/server/src/provider/CodexToolPresentation.ts+16-20
...@@ -6,25 +6,18 @@ import type {...@@ -6,25 +6,18 @@ import type {
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import type * as EffectCodexSchema from "effect-codex-app-server/schema";7import type * as EffectCodexSchema from "effect-codex-app-server/schema";
88
9import {
10 mcpToolPresentation as integrationToolPresentation,
11 normalizeMcpHttpUrl as normalizedHttpUrl,
12 normalizeMcpText as normalizedDisplayName,
13} from "./McpToolPresentation.ts";
14
9type CodexLifecycleItem = EffectCodexSchema.V2ItemCompletedNotification["item"];15type CodexLifecycleItem = EffectCodexSchema.V2ItemCompletedNotification["item"];
1016
11function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {17function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {
12 return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;18 return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
13}19}
1420
15function normalizedHttpUrl(value: unknown): string | undefined {
16 if (typeof value !== "string" || value.length > 4096) return undefined;
17 try {
18 const url = new URL(value);
19 const href = url.href;
20 return (url.protocol === "http:" || url.protocol === "https:") && href.length <= 4096
21 ? href
22 : undefined;
23 } catch {
24 return undefined;
25 }
26}
27
28function normalizedImageUrl(value: unknown): string | undefined {21function normalizedImageUrl(value: unknown): string | undefined {
29 if (typeof value !== "string" || value.length > 4096) return undefined;22 if (typeof value !== "string" || value.length > 4096) return undefined;
30 try {23 try {
...@@ -45,12 +38,6 @@ function normalizedAppId(value: unknown): string | undefined {...@@ -45,12 +38,6 @@ function normalizedAppId(value: unknown): string | undefined {
45 : undefined;38 : undefined;
46}39}
4740
48function normalizedDisplayName(value: unknown): string | undefined {
49 if (typeof value !== "string") return undefined;
50 const displayName = value.trim().replace(/\s+/gu, " ");
51 return displayName && displayName.length <= 160 ? displayName : undefined;
52}
53
54function normalizedSourceKeyPart(value: string): string {41function normalizedSourceKeyPart(value: string): string {
55 return value.trim().toLowerCase();42 return value.trim().toLowerCase();
56}43}
...@@ -130,6 +117,7 @@ function themedLogoIcon(...@@ -130,6 +117,7 @@ function themedLogoIcon(
130}117}
131118
132export interface McpToolPresentation {119export interface McpToolPresentation {
120 readonly title?: string;
133 readonly toolSurface?: "browser" | "computer";121 readonly toolSurface?: "browser" | "computer";
134 readonly toolIcon?: ToolActivityIcon;122 readonly toolIcon?: ToolActivityIcon;
135 readonly toolSource?: ToolActivitySource;123 readonly toolSource?: ToolActivitySource;
...@@ -227,5 +215,13 @@ export function mcpToolPresentation(...@@ -227,5 +215,13 @@ export function mcpToolPresentation(
227 };215 };
228 }216 }
229217
230 return {};218 return integrationToolPresentation({
219 serverName: appContext?.connectorId ?? item.server,
220 toolName: item.tool,
221 title: appContext?.actionName,
222 serverDisplayName: appContext?.appName,
223 source: sourceMetadata,
224 iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined,
225 iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined,
226 });
231}227}
apps/server/src/provider/McpToolPresentation.test.ts created+103
...@@ -0,0 +1,103 @@
1import { describe, expect, it } from "@effect/vitest";
2import { mcpToolPresentation } from "./McpToolPresentation.ts";
3
4describe("mcpToolPresentation", () => {
5 it("uses supplied names and logos while retaining the server identity", () => {
6 expect(
7 mcpToolPresentation({
8 serverName: "firecrawl-local",
9 toolName: "firecrawl_scrape",
10 title: " Scrape\n page ",
11 serverDisplayName: "Firecrawl",
12 iconUrl: "https://example.com/icon.png",
13 iconUrlDark: "https://example.com/dark.png",
14 }),
15 ).toEqual({
16 title: "Scrape page",
17 toolIcon: {
18 _tag: "themed-logo",
19 logoUrl: "https://example.com/icon.png",
20 logoUrlDark: "https://example.com/dark.png",
21 },
22 toolSource: {
23 key: "mcp:firecrawl-local",
24 name: "Firecrawl",
25 kind: "integration",
26 icon: {
27 _tag: "themed-logo",
28 logoUrl: "https://example.com/icon.png",
29 logoUrlDark: "https://example.com/dark.png",
30 },
31 },
32 });
33 });
34
35 it("uses MCP result source metadata for the tool and its integration", () => {
36 const presentation = mcpToolPresentation({
37 serverName: "firecrawl-local",
38 toolName: "firecrawl_scrape",
39 source: {
40 name: "Firecrawl",
41 logoUrl: "https://example.com/firecrawl.png",
42 logoUrlDark: "https://example.com/firecrawl-dark.png",
43 },
44 });
45 expect(presentation.toolSource?.name).toBe("Firecrawl");
46 expect(presentation.toolIcon).toEqual({
47 _tag: "themed-logo",
48 logoUrl: "https://example.com/firecrawl.png",
49 logoUrlDark: "https://example.com/firecrawl-dark.png",
50 });
51 expect(presentation.toolSource?.icon).toEqual(presentation.toolIcon);
52 });
53
54 it("uses readable fallback names without a guessed logo", () => {
55 expect(mcpToolPresentation({ toolName: "mcp__my_server__get_weather" })).toEqual({
56 title: "get weather",
57 toolSource: { key: "mcp:my_server", name: "my server", kind: "integration" },
58 });
59 expect(mcpToolPresentation({ toolName: "Read" })).toEqual({});
60 expect(mcpToolPresentation({ serverName: "t3-code", toolName: "delegate_task" })).toEqual({});
61 });
62
63 it.each([
64 "javascript:alert(1)",
65 "file:///tmp/logo.png",
66 "not a URL",
67 "https://example.com/" + "x".repeat(4096),
68 ])("ignores an invalid logo %s", (iconUrl) => {
69 expect(
70 mcpToolPresentation({
71 serverName: "weather",
72 toolName: "get_weather",
73 iconUrl,
74 title: "x".repeat(161),
75 }),
76 ).toEqual({
77 title: "get weather",
78 toolSource: { key: "mcp:weather", name: "weather", kind: "integration" },
79 });
80 });
81
82 it("rejects malformed and overlong names", () => {
83 expect(mcpToolPresentation({ serverName: {}, toolName: 1, title: [] })).toEqual({});
84 expect(mcpToolPresentation({ serverName: "x".repeat(161), toolName: "get_weather" })).toEqual(
85 {},
86 );
87 expect(mcpToolPresentation({ title: "Tool title" })).toEqual({ title: "Tool title" });
88 });
89
90 it.each([
91 null,
92 "logo",
93 { logoUrl: "file:///tmp/logo.png" },
94 { logoUrlDark: "https://example.com/dark.png" },
95 ])("ignores source metadata without a valid logo: %j", (source) => {
96 expect(mcpToolPresentation({ serverName: "weather", toolName: "get_weather", source })).toEqual(
97 {
98 title: "get weather",
99 toolSource: { key: "mcp:weather", name: "weather", kind: "integration" },
100 },
101 );
102 });
103});
apps/server/src/provider/McpToolPresentation.ts created+70
...@@ -0,0 +1,70 @@
1import type { ToolActivityIcon, ToolActivitySource } from "@t3tools/contracts";
2import { resolveT3McpToolDefinition } from "@t3tools/shared/t3McpToolPresentation";
3
4export function normalizeMcpText(value: unknown, maxLength = 160): string | undefined {
5 if (typeof value !== "string") return undefined;
6 const text = value.trim().replace(/\s+/gu, " ");
7 return text.length > 0 && text.length <= maxLength ? text : undefined;
8}
9
10export function normalizeMcpHttpUrl(value: unknown): string | undefined {
11 if (typeof value !== "string" || value.length > 4096) return undefined;
12 try {
13 const url = new URL(value);
14 return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096
15 ? url.href
16 : undefined;
17 } catch {
18 return undefined;
19 }
20}
21
22export function mcpToolPresentation(input: {
23 readonly toolName?: unknown;
24 readonly serverName?: unknown;
25 readonly title?: unknown;
26 readonly serverDisplayName?: unknown;
27 readonly iconUrl?: unknown;
28 readonly iconUrlDark?: unknown;
29 readonly source?: unknown;
30}): {
31 readonly title?: string;
32 readonly toolIcon?: ToolActivityIcon;
33 readonly toolSource?: ToolActivitySource;
34} {
35 const source =
36 typeof input.source === "object" && input.source !== null ? input.source : undefined;
37 const qualified =
38 typeof input.toolName === "string" ? /^mcp__(.+?)__(.+)$/i.exec(input.toolName) : null;
39 const server = normalizeMcpText(input.serverName ?? qualified?.[1] ?? input.serverDisplayName);
40 const tool = normalizeMcpText(qualified?.[2] ?? input.toolName);
41 if (server && tool && resolveT3McpToolDefinition(`${server}.${tool}`)) return {};
42 const title =
43 normalizeMcpText(input.title) ??
44 (server && tool ? normalizeMcpText(tool.replace(/[_-]+/gu, " ")) : undefined);
45 if (!server) return title ? { title } : {};
46 const name =
47 normalizeMcpText(input.serverDisplayName) ??
48 normalizeMcpText(source && Reflect.get(source, "name")) ??
49 normalizeMcpText(server.replace(/[_-]+/gu, " ")) ??
50 server;
51 const logoUrl =
52 normalizeMcpHttpUrl(input.iconUrl) ??
53 normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrl"));
54 const logoUrlDark =
55 normalizeMcpHttpUrl(input.iconUrlDark) ??
56 normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrlDark"));
57 const icon = logoUrl
58 ? { _tag: "themed-logo" as const, logoUrl, ...(logoUrlDark ? { logoUrlDark } : {}) }
59 : undefined;
60 return {
61 ...(title ? { title } : {}),
62 ...(icon ? { toolIcon: icon } : {}),
63 toolSource: {
64 key: `mcp:${server.toLowerCase()}`,
65 name,
66 kind: "integration",
67 ...(icon ? { icon } : {}),
68 },
69 };
70}
apps/server/src/provider/acp/AcpRuntimeModel.test.ts+42-4
...@@ -1539,9 +1539,44 @@ describe("extractMcpToolCallIdentity", () => {...@@ -1539,9 +1539,44 @@ describe("extractMcpToolCallIdentity", () => {
1539 });1539 });
1540 });1540 });
15411541
1542 it("does not brand tools whose meta asserts a foreign server", () => {1542 it.each([
1543 // The foreign assertion vetoes every loose source, including a title1543 { _meta: { claudeCode: { toolName: "mcp__weather__get_weather" } } },
1544 // that would otherwise match a T3 convention.1544 { _meta: { toolName: "mcp::weather::get_weather", serverId: "weather" } },
1545 ])("recovers external MCP identity from provider metadata", (metadata) => {
1546 const toolCall = toolCallFromUpdate({
1547 sessionUpdate: "tool_call",
1548 toolCallId: "weather-call",
1549 title: "Checking the forecast",
1550 status: "in_progress",
1551 ...metadata,
1552 });
1553 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1554 server: "weather",
1555 tool: "get_weather",
1556 });
1557 });
1558
1559 it.each([
1560 { kind: "execute", toolName: "developer__shell", extensionName: "developer" },
1561 { kind: "edit", toolName: "developer__edit", extensionName: "developer" },
1562 { kind: "other", toolName: "weather__get_weather", extensionName: "weather" },
1563 ] as const)(
1564 "leaves goose $toolName unclassified so built-ins keep their projection",
1565 ({ kind, toolName, extensionName }) => {
1566 // goose tags built-in extensions exactly like user MCP servers.
1567 const toolCall = toolCallFromUpdate({
1568 sessionUpdate: "tool_call",
1569 toolCallId: "goose-builtin",
1570 kind,
1571 title: "developer: shell",
1572 status: "in_progress",
1573 _meta: { goose: { toolCall: { toolName, extensionName } } },
1574 });
1575 expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined();
1576 },
1577 );
1578
1579 it("uses the asserted server instead of a misleading T3 title", () => {
1545 const toolCall = toolCallFromUpdate({1580 const toolCall = toolCallFromUpdate({
1546 sessionUpdate: "tool_call",1581 sessionUpdate: "tool_call",
1547 toolCallId: "foreign-1",1582 toolCallId: "foreign-1",
...@@ -1551,7 +1586,10 @@ describe("extractMcpToolCallIdentity", () => {...@@ -1551,7 +1586,10 @@ describe("extractMcpToolCallIdentity", () => {
1551 _meta: { toolName: "delegate_task", serverId: "other-orchestrator" },1586 _meta: { toolName: "delegate_task", serverId: "other-orchestrator" },
1552 });1587 });
15531588
1554 expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined();1589 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1590 server: "other-orchestrator",
1591 tool: "delegate_task",
1592 });
1555 });1593 });
15561594
1557 it("does not brand path-like or unknown-tool titles", () => {1595 it("does not brand path-like or unknown-tool titles", () => {
apps/server/src/provider/acp/AcpRuntimeModel.ts+14-6
...@@ -1134,9 +1134,7 @@ export function extractMcpToolCallIdentity(...@@ -1134,9 +1134,7 @@ export function extractMcpToolCallIdentity(
1134 // (goose, qwen, claude-acp) or only through the namespaced function name1134 // (goose, qwen, claude-acp) or only through the namespaced function name
1135 // in the title. The verbatim wire title survives merges even when a later1135 // in the title. The verbatim wire title survives merges even when a later
1136 // titleless or LLM-enriched update replaces the presentation title, so1136 // titleless or LLM-enriched update replaces the presentation title, so
1137 // match those rather than the summarized state title. Name-derived matches1137 // match those rather than the summarized state title.
1138 // are gated on the known T3 tool inventory so path-like titles (for
1139 // example "t3-code/README.md") never brand.
1140 const claudeCode = isRecord(meta?.claudeCode) ? meta.claudeCode : undefined;1138 const claudeCode = isRecord(meta?.claudeCode) ? meta.claudeCode : undefined;
1141 const gooseToolCall = isRecord(meta?.goose)1139 const gooseToolCall = isRecord(meta?.goose)
1142 ? isRecord(meta.goose.toolCall)1140 ? isRecord(meta.goose.toolCall)
...@@ -1160,15 +1158,22 @@ export function extractMcpToolCallIdentity(...@@ -1160,15 +1158,22 @@ export function extractMcpToolCallIdentity(
1160 }1158 }
1161 }1159 }
1162 }1160 }
1163 // A present-but-foreign origin assertion marks the whole call as another
1164 // server's MCP call, so no loose name matching (meta or title) may brand it.
1165 const gooseExtension =1161 const gooseExtension =
1166 typeof gooseToolCall?.extensionName === "string" ? gooseToolCall.extensionName.trim() : "";1162 typeof gooseToolCall?.extensionName === "string" ? gooseToolCall.extensionName.trim() : "";
1167 const assertsForeignOrigin =1163 const assertsForeignOrigin =
1168 (metaServerId.length > 0 && !/^t3[-_ ]?code$/i.test(metaServerId)) ||1164 (metaServerId.length > 0 && !/^t3[-_ ]?code$/i.test(metaServerId)) ||
1169 (gooseExtension.length > 0 && !/^t3[-_ ]?code$/i.test(gooseExtension));1165 (gooseExtension.length > 0 && !/^t3[-_ ]?code$/i.test(gooseExtension));
1166 // A foreign origin never brands as T3. qwen's serverId marks a real MCP
1167 // server, but goose reports its built-in extensions (developer__shell,
1168 // edits) the same way as user MCP servers, so goose stays unclassified and
1169 // keeps its command and file-change projections.
1170 if (assertsForeignOrigin) {1170 if (assertsForeignOrigin) {
1171 return undefined;1171 if (metaServerId.length === 0 || metaToolName.length === 0) return undefined;
1172 const prefix = [`mcp__${metaServerId}__`, `mcp::${metaServerId}::`].find((prefix) =>
1173 metaToolName.startsWith(prefix),
1174 );
1175 const tool = prefix === undefined ? metaToolName : metaToolName.slice(prefix.length);
1176 return tool ? { server: metaServerId, tool } : undefined;
1172 }1177 }
1173 const candidates = [1178 const candidates = [
1174 meta?.toolName,1179 meta?.toolName,
...@@ -1178,6 +1183,9 @@ export function extractMcpToolCallIdentity(...@@ -1178,6 +1183,9 @@ export function extractMcpToolCallIdentity(
1178 ].filter((value): value is string => typeof value === "string");1183 ].filter((value): value is string => typeof value === "string");
1179 for (const candidate of candidates) {1184 for (const candidate of candidates) {
1180 const trimmed = candidate.trim();1185 const trimmed = candidate.trim();
1186 const qualified = /^mcp__(.+?)__(.+)$/i.exec(trimmed);
1187 if (qualified?.[1] && qualified[2] && !/^t3[-_ ]?code$/i.test(qualified[1]))
1188 return { server: qualified[1], tool: qualified[2] };
1181 const match =1189 const match =
1182 T3_MCP_TITLE_CALL.exec(trimmed) ??1190 T3_MCP_TITLE_CALL.exec(trimmed) ??
1183 T3_MCP_TITLE_SUFFIX_CALL.exec(trimmed) ??1191 T3_MCP_TITLE_SUFFIX_CALL.exec(trimmed) ??