From 78a75cf0002676ee99855eeaa53d31ce34a3beaf Mon Sep 17 00:00:00 2001 From: scratchyone <11479077+scratchyone@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:05 -0400 Subject: [PATCH 1/5] fix(server): Claude MCP tools show their names and icons Claude Code sends each MCP tool's display name, server name and icon as tool_use_meta on assistant frames. The Claude adapter now uses them for the tool item's title and source, so rows read "Firecrawl scrape" with the Firecrawl logo instead of mcp__claude_ai_Firecrawl__firecrawl_scrape. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../record-claude-agent-sdk-replay-fixture.ts | 9 ++ .../Adapters/ClaudeAdapterV2.testkit.ts | 5 + .../Adapters/ClaudeAdapterV2.ts | 105 +++++++++++++++++- .../ClaudeReplayFixtures.integration.test.ts | 3 +- .../claude_transcript.ndjson | 94 ++++++++++++++++ .../claude_mcp_tool_presentation/input.ts | 14 +++ .../claude_mcp_tool_presentation/output.ts | 51 +++++++++ .../testkit/fixtures/index.ts | 17 +++ 8 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson create mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts create mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts diff --git a/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts b/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts index 2d5e348a35b7..aa0fafeb9a36 100644 --- a/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts +++ b/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts @@ -68,6 +68,7 @@ import { CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT, } from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/input.ts"; import { CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_monitor_wake/input.ts"; +import { CLAUDE_MCP_TOOL_PRESENTATION_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts"; import { CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_task_interrupt/input.ts"; import { CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_wake_before_queued_prompt/input.ts"; import { @@ -237,6 +238,14 @@ const CLAUDE_RECORDINGS = { interruptAfter: "tool_use", interruptAfterToolUses: 2, }, + // Needs the claude.ai Firecrawl connector on the recording account. Claude + // Code describes MCP tool uses in an undeclared `tool_use_meta` field. + claude_mcp_tool_presentation: { + prompts: [CLAUDE_MCP_TOOL_PRESENTATION_PROMPT], + defaultTranscriptFile: "fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, subagent: { prompts: [SUBAGENT_PROMPT], defaultTranscriptFile: "fixtures/subagent/claude_transcript.ndjson", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index ba87135a84d0..29ce8546bc87 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -1120,6 +1120,11 @@ function sanitizeSdkMessageForReplay(input: { session_id: message.session_id, }; } + // Like init's slash_commands, the recording account's commands and skills + // are local configuration, not protocol. + if (message.type === "system" && message.subtype === "commands_changed") { + return { ...message, commands: [] }; + } if (message.type === "rate_limit_event") { return { ...message, diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 68dda79bd821..a8a60badc263 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -63,6 +63,7 @@ import { type ProviderUserInputAnswers, type ProviderThreadId, type ThreadId, + type ToolActivitySource, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; @@ -2031,6 +2032,64 @@ function claudeToolUseBlocksFromAssistantMessage( return message.message.content.filter(isClaudeToolUseContentBlock); } +export interface ClaudeToolPresentation { + readonly title: string; + readonly toolSource?: ToolActivitySource; +} + +function boundedMetaText(value: unknown, maxLength: number): string | undefined { + if (typeof value !== "string") return undefined; + const text = value.trim().replace(/\s+/gu, " "); + return text.length > 0 && text.length <= maxLength ? text : undefined; +} + +function metaHttpUrl(value: unknown): string | undefined { + if (typeof value !== "string" || value.length > 4096) return undefined; + try { + const url = new URL(value); + return url.protocol === "https:" || url.protocol === "http:" ? url.href : undefined; + } catch { + return undefined; + } +} + +/** + * Claude Code's own label for each tool use (the MCP tool's title, or its + * humanized name) plus the server's name and icon. The CLI sends these as + * `tool_use_meta` on assistant frames; the SDK types do not declare it yet. + */ +export function claudeToolPresentationsFromAssistantMessage( + message: SDKMessage, +): ReadonlyMap { + const presentations = new Map(); + const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined; + if (!Array.isArray(meta)) return presentations; + for (const entry of meta) { + if (typeof entry !== "object" || entry === null) continue; + const id = boundedMetaText(Reflect.get(entry, "id"), 512); + const title = boundedMetaText(Reflect.get(entry, "display_name"), 160); + if (id === undefined || title === undefined) continue; + const serverName = boundedMetaText(Reflect.get(entry, "server_display_name"), 160); + const iconUrl = metaHttpUrl(Reflect.get(entry, "icon_url")); + presentations.set(id, { + title, + ...(serverName === undefined + ? {} + : { + toolSource: { + key: `mcp:${serverName.toLowerCase()}`, + name: serverName, + kind: "integration", + ...(iconUrl === undefined + ? {} + : { icon: { _tag: "themed-logo", logoUrl: iconUrl } as const }), + }, + }), + }); + } + return presentations; +} + function claudeToolResultBlocksFromAssistantMessage( message: SDKMessage, ): ReadonlyArray { @@ -2599,6 +2658,7 @@ interface ActiveClaudeToolCall { readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; readonly ordinal: number; readonly startedAt: DateTime.Utc; + readonly presentation?: ClaudeToolPresentation; } const PENDING_CLAUDE_SUBAGENT_MODEL_CAP = 64; @@ -3540,6 +3600,7 @@ export function makeClaudeAdapterV2( >; readonly startedAt: DateTime.Utc; readonly updatedAt: DateTime.Utc; + readonly presentation: ClaudeToolPresentation | undefined; }) => { const completedAt = input.status === "running" ? null : input.updatedAt; const nodeId = idAllocator.derive.nodeFromProviderItem({ @@ -3597,7 +3658,10 @@ export function makeClaudeAdapterV2( parentItemId: null, ordinal: input.ordinal, status: input.status, - title: readPath !== undefined ? formatReadToolLabel(readPath) : (searchTitle ?? null), + title: + readPath !== undefined + ? formatReadToolLabel(readPath) + : (searchTitle ?? input.presentation?.title ?? null), startedAt: input.startedAt, completedAt, updatedAt: input.updatedAt, @@ -3660,6 +3724,9 @@ export function makeClaudeAdapterV2( : { ...itemBase, type: "dynamic_tool", + ...(input.presentation?.toolSource === undefined + ? {} + : { toolSource: input.presentation.toolSource }), toolName: input.toolName, ...(viewedImagePath === undefined ? {} : { viewedImagePath }), input: claudeNativeToolInputValue(input.toolInput), @@ -4324,10 +4391,38 @@ export function makeClaudeAdapterV2( readonly toolName: string; readonly toolInput: ClaudeNativeToolInput; readonly parentToolUseId: string | null; + readonly presentation?: ClaudeToolPresentation | undefined; }) { const existing = input.context.toolCalls.get(input.nativeItemId); if (existing !== undefined) { - return existing; + // The permission callback can start a call before its assistant + // frame arrives with the tool's display name and icon. + if (input.presentation === undefined || existing.presentation !== undefined) { + return existing; + } + const presented = { ...existing, presentation: input.presentation }; + input.context.toolCalls.set(input.nativeItemId, presented); + const updatedAt = yield* DateTime.now; + yield* emitToolCallArtifacts( + buildToolCallArtifacts({ + context: input.context, + nativeItemId: presented.nativeItemId, + toolName: presented.toolName, + classification: presented.classification, + toolInput: presented.input, + threadId: presented.threadId, + runId: presented.runId, + rootNodeId: presented.rootNodeId, + parentNodeId: presented.parentNodeId, + ordinal: presented.ordinal, + output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, + status: "running", + startedAt: presented.startedAt, + updatedAt, + presentation: input.presentation, + }), + ); + return presented; } const startedAt = yield* DateTime.now; const classification = classifyClaudeNativeTool(input.toolName); @@ -4354,6 +4449,7 @@ export function makeClaudeAdapterV2( parentNodeId, ordinal, startedAt, + ...(input.presentation === undefined ? {} : { presentation: input.presentation }), }; input.context.toolCalls.set(input.nativeItemId, toolCall); yield* emitToolCallArtifacts( @@ -4372,6 +4468,7 @@ export function makeClaudeAdapterV2( status: "running", startedAt, updatedAt: startedAt, + presentation: input.presentation, }), ); return toolCall; @@ -4560,6 +4657,7 @@ export function makeClaudeAdapterV2( status: "failed", startedAt: toolCall.startedAt, updatedAt: input.completedAt, + presentation: toolCall.presentation, }); yield* emitToolCallArtifacts(artifacts); } @@ -5726,6 +5824,7 @@ export function makeClaudeAdapterV2( } } + const toolPresentations = claudeToolPresentationsFromAssistantMessage(message); for (const toolUse of claudeToolUseBlocksFromAssistantMessage(message)) { const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input); if (toolUse.name === "Agent") { @@ -5746,6 +5845,7 @@ export function makeClaudeAdapterV2( toolName: toolUse.name, toolInput: nativeToolInput, parentToolUseId: parentToolUseIdFromSdkMessage(message), + presentation: toolPresentations.get(toolUse.id), }); const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id); if (heldPlan !== undefined) { @@ -5809,6 +5909,7 @@ export function makeClaudeAdapterV2( status: isClaudeToolResultError(toolResult) ? "failed" : "completed", startedAt: toolCall.startedAt, updatedAt: completedAt, + presentation: toolCall.presentation, }); yield* emitToolCallArtifacts(artifacts); context.toolCalls.delete(toolCall.nativeItemId); diff --git a/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts index 3753206c94df..ec368f829fea 100644 --- a/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts @@ -211,7 +211,8 @@ describe("Claude Agent SDK replay fixtures", () => { for (const toolName of claudeToolUseNamesFromTranscript(transcript)) { seenToolNames.add(toolName); const classification = classifyClaudeNativeTool(toolName); - if (!classification.known) { + // MCP tools are open-ended and deliberately become dynamic tools. + if (!classification.known && !toolName.startsWith("mcp__")) { unknownToolNames.add(`${fixture.name}:${toolName}`); } } diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson new file mode 100644 index 000000000000..150d6f4866fa --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson @@ -0,0 +1,94 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"claude_mcp_tool_presentation","metadata":{"prompts":["Do exactly this, one tool call at a time, with no other tool calls:\n1) Call the Firecrawl firecrawl_scrape tool on https://example.com with formats [\"markdown\"].\n2) Call the Firecrawl firecrawl_map tool on https://example.com with limit 1.\n3) Reply with exactly: mcp presentation fixture complete"],"model":"claude-sonnet-4-6","nativeSessionId":"e51402d3-25be-4866-81d5-2ab30cecb268","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","handEdits":["Removed tool_use_meta from the firecrawl_map assistant frame, standing in for a Claude Code build that sends no display metadata for an MCP tool."],"generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","settings":{"showThinkingSummaries":true},"allowDangerouslySkipPermissions":true,"sessionId":"e51402d3-25be-4866-81d5-2ab30cecb268"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Do exactly this, one tool call at a time, with no other tool calls:\n1) Call the Firecrawl firecrawl_scrape tool on https://example.com with formats [\"markdown\"].\n2) Call the Firecrawl firecrawl_map tool on https://example.com with limit 1.\n3) Reply with exactly: mcp presentation fixture complete"},"parent_tool_use_id":null,"uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}}} +{"type":"emit_inbound","label":"command_lifecycle","frame":{"type":"command_lifecycle","command_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a","state":"queued","uuid":"042fc299-7a91-42e7-a7c8-7ad83c342c61","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"16a8e60b-317a-4de1-b45e-2707aebd957e","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"commands_changed","commands":[],"uuid":"5882c810-a76d-43ef-a7d8-20a4ce7272f7","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268"}} +{"type":"emit_inbound","label":"command_lifecycle","frame":{"type":"command_lifecycle","command_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a","state":"started","uuid":"0b876c44-1d79-45b9-84e1-8ac9d006e1eb","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.282","cwd":"/tmp/claude-replay-claude_mcp_tool_presentation","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"17b07fd4-a49e-4f29-908d-2eb4e79079f2","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"status","status":"requesting","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"25df71c1-b001-403e-bd02-20f7a131e6be"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyMTdhN1SHi8KRaXCaz","type":"message","role":"assistant","content":[],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":13908,"cache_read_input_tokens":120650,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":13908},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"ee9a690a-66c6-419c-beb6-178282c2b2d6","ttft_ms":1398,"user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a","user_message_uuids":["8935142f-0551-46e6-9a6b-95034332fa1a"]}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"5531e320-0b9f-44ed-bb84-9217aec3a7de"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":1,"estimated_tokens_delta":1,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"e131358c-9526-42f7-9893-a9f21c8da7b1","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"4e3389b2-8998-4002-9b79-7e4cba7710cf"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":6,"estimated_tokens_delta":5,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"c8a1f6f6-779c-4464-944c-224cfa5ff337","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" user wants me to do","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"b0fc7c7a-7e9f-4703-be79-2a84a1ae779c"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":10,"estimated_tokens_delta":4,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"b8bff8ce-d9d9-42cf-8531-eca61d512198","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" exactly three","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"8d7902d0-2c10-4f31-b104-325f009187a7"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":12,"estimated_tokens_delta":2,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"803f3856-8179-4080-9fbd-cbd2446c94e6","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" things","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"27f9d2ee-21cb-4d7d-8ef6-e2dd7c8c013b"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":13,"estimated_tokens_delta":1,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"b15bdd50-2536-4142-89d4-0968478a199d","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" in","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"c8d8ea67-b12c-47f8-b3b2-39e6a1d649bb"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":16,"estimated_tokens_delta":3,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"9f07ec5f-2c77-4f10-9732-86d72b2023ce","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" sequence,","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"5678559a-f58c-4ad5-a601-ea66ddf38a29"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":23,"estimated_tokens_delta":7,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"c0512ed5-0470-4688-b424-d87248fc558b","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" one tool call at a time:","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"488b1f98-da86-496f-8808-b97a17e548eb"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":26,"estimated_tokens_delta":3,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"53e7bbf1-7787-436d-bb2f-fb227d9ec08e","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"\n1. Call fi","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"95bcdca5-05da-4af8-8476-db503f8634d3"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":31,"estimated_tokens_delta":5,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"1b44ef1e-7a1b-4f9f-b31f-2827e1cd4b98","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"recrawl_scrape on","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"0ca78758-21a6-4278-8647-17346dc8b7a9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":40,"estimated_tokens_delta":9,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"263d1905-60db-44fb-bb8a-aff133cfd1c5","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" https://example.com with formats [\"","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"2a487a60-db75-4949-bf04-abd72be141c9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":46,"estimated_tokens_delta":6,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"26b28799-60aa-4402-b4cf-6131ac9208c0","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"markdown\"]\n2. Call firec","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"47c553fa-1067-4ec1-9d03-43c179ab77c1"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":53,"estimated_tokens_delta":7,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"8b22a64d-0ff3-42a0-8bf9-cf334970484c","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"rawl_map on https://example","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"85ff3f48-9b5f-4bfb-9424-304286e6feb9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":58,"estimated_tokens_delta":5,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"27537bbc-ea6d-4ab2-82fa-a1d155e47c24","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":".com with limit 1\n3","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"9228b117-54b5-4735-91c0-36c2c8ab3589"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":62,"estimated_tokens_delta":4,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"dc661ed1-0053-4137-9ba3-b047fc5673a2","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":". Reply with \"","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"1f3a14f3-ab46-4901-bd67-01ccc56e9e53"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":72,"estimated_tokens_delta":10,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"c8752e2c-b468-4565-83d6-b0cf93958dcc","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"mcp presentation fixture complete\"\n\nLet","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"799ac3de-dc15-456b-ba7f-7ed646091650"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":74,"estimated_tokens_delta":2,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"252003d8-27ca-459d-83d2-76c3954ab781","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" me do","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"98477c6a-267d-47ec-9ab9-23b6ac9ed9be"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":76,"estimated_tokens_delta":2,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"79faa185-c65f-4d35-902b-e90c67e71328","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" these","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"61e9467c-47a6-47ac-b1a8-3fbd2c4017cb"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":80,"estimated_tokens_delta":4,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"62df28bc-7825-4c68-bf8c-2c55330e8bcd","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" one at a time.","estimated_tokens":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"e81841c1-d71a-4f05-8e3e-ccc487761c94"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"thinking_tokens","estimated_tokens":144,"estimated_tokens_delta":64,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"7c601f46-605b-46d4-afcd-459d3a0778a4","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"ErcECqoBCBIYAipAr/7SWAcYeXs7o2aRtpsjID+2LXjJzbSBehaQmjIpalZdQ/zDlKo5HdSV5/hjsXI5PLCptKaONWLOUH72laOx2jIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJGFmOTIzNWNiLWRjNTItNDViMC05MTI2LTNmZTBkZmJmNzNkY3IQOUPUj6Q5Zqq7Ny6ZW11Rk4gBAagBgLTv1QawAQISDFxgTUNaIxrJGThAcxoM1pccePQyi26dbGkQIjBasphSlIQMWY/cGUpkz8rfvlDWGHElBlHgWxNzbEs5sHqyq/gMPFYpuu1VfCUuRXgquQKpt01uhZkzPA91ZoWdnE2uwB9CP7DrueotB4xeqPC8If0jI7EU2E52jEO9FJvPWKvrv40DPI9dUyAT82i3QfMfVbmslyw8lWv8JFmjod4MPRdI3DBj8zDztU+YRjtKMiy91u5NG5oJyI4M8DLrsVD7WRpA3BEb5vREl15X8T41rhYh14w8TXglDpEXgwPsJUd6+oJhR3dYrX71pdj6JILMrh0xQ1WNVm+fSXDIYxCUV8wmerH1ewTP4FwLip1icXtGi8lQSYjKT0hnNlPgMG13AbSMM/dI3A50XB8FighdGSaY+OoKZuZWfOA6XeNRUqKi6qaUb5eJnV0UsvSfxe++TgvGiaQubZ0NQm0GRcSz5LHJBxccNcaLBnqEp5l4Y7fQ2Lo/Kwzgndnq21jXyVpPEd0OcsQOzCxsGAE="}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"941464bd-9b05-453c-8ee7-1529998aac44"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyMTdhN1SHi8KRaXCaz","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to do exactly three things in sequence, one tool call at a time:\n1. Call firecrawl_scrape on https://example.com with formats [\"markdown\"]\n2. Call firecrawl_map on https://example.com with limit 1\n3. Reply with \"mcp presentation fixture complete\"\n\nLet me do these one at a time.","signature":"ErcECqoBCBIYAipAr/7SWAcYeXs7o2aRtpsjID+2LXjJzbSBehaQmjIpalZdQ/zDlKo5HdSV5/hjsXI5PLCptKaONWLOUH72laOx2jIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJGFmOTIzNWNiLWRjNTItNDViMC05MTI2LTNmZTBkZmJmNzNkY3IQOUPUj6Q5Zqq7Ny6ZW11Rk4gBAagBgLTv1QawAQISDFxgTUNaIxrJGThAcxoM1pccePQyi26dbGkQIjBasphSlIQMWY/cGUpkz8rfvlDWGHElBlHgWxNzbEs5sHqyq/gMPFYpuu1VfCUuRXgquQKpt01uhZkzPA91ZoWdnE2uwB9CP7DrueotB4xeqPC8If0jI7EU2E52jEO9FJvPWKvrv40DPI9dUyAT82i3QfMfVbmslyw8lWv8JFmjod4MPRdI3DBj8zDztU+YRjtKMiy91u5NG5oJyI4M8DLrsVD7WRpA3BEb5vREl15X8T41rhYh14w8TXglDpEXgwPsJUd6+oJhR3dYrX71pdj6JILMrh0xQ1WNVm+fSXDIYxCUV8wmerH1ewTP4FwLip1icXtGi8lQSYjKT0hnNlPgMG13AbSMM/dI3A50XB8FighdGSaY+OoKZuZWfOA6XeNRUqKi6qaUb5eJnV0UsvSfxe++TgvGiaQubZ0NQm0GRcSz5LHJBxccNcaLBnqEp5l4Y7fQ2Lo/Kwzgndnq21jXyVpPEd0OcsQOzCxsGAE="}],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":13908,"cache_read_input_tokens":120650,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":13908},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"95deaf17-0eb1-4b10-9b64-4b3d86784821","timestamp":"2026-09-29T15:32:16.925Z","user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a","user_message_uuids":["8935142f-0551-46e6-9a6b-95034332fa1a"]}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_stop","index":0},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"e33d797d-cb58-4208-b4a1-6997bf585ee2"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01CqVNFCESeShHVUiLMLPsCg","name":"mcp__claude_ai_Firecrawl__firecrawl_scrape","input":{},"caller":{"type":"direct"}}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"9d628e2d-81bf-4206-b587-0cff5caf1aed"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"b5f7700a-9aff-44cf-96d9-1655cad51f8a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"url\":"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"fc274f0d-de9c-44d8-b142-4f8e21bd3427"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"https"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"41ef7bfc-f8c4-4c0d-98d9-146e4a4cc394"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"://exam"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"9a63da1a-67d1-43d6-951d-f1c60e112a62"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"ple.com"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"7891419a-e906-43be-84b0-4b2f16be6223"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"1a5d24aa-f26c-4e17-b5cc-f122c8261b2e"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":", \"form"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"229e2bfe-a912-4a27-9f25-49f14fdafe24"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"ats\": ["}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"cf50db11-2827-4ed7-8407-0cbecb3c8c31"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"markdo"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"39a14b30-36a6-462c-ad1d-1a07bf08403d"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"wn\"]}"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"a9ed2753-02fb-48cc-9425-d0ea87e23fd1"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyMTdhN1SHi8KRaXCaz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CqVNFCESeShHVUiLMLPsCg","name":"mcp__claude_ai_Firecrawl__firecrawl_scrape","input":{"url":"https://example.com","formats":["markdown"]},"caller":{"type":"direct"}}],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":13908,"cache_read_input_tokens":120650,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":13908},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"61e61e08-0d57-4e0b-9685-0000f4eb29a0","timestamp":"2026-09-29T15:32:17.004Z","wire_tool_inputs":{"toolu_01CqVNFCESeShHVUiLMLPsCg":{"url":"https://example.com","formats":["markdown"]}},"tool_use_meta":[{"id":"toolu_01CqVNFCESeShHVUiLMLPsCg","display_name":"Firecrawl scrape","server_display_name":"Firecrawl","icon_url":"https://www.google.com/s2/favicons?domain=firecrawl.dev&sz=64"}]}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_stop","index":1},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"2429ac0f-a65e-401a-a3b1-3eed4a37f28d"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"container":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":13908,"cache_read_input_tokens":120650,"output_tokens":191,"output_tokens_details":{"thinking_tokens":95},"iterations":[{"input_tokens":3,"output_tokens":191,"cache_read_input_tokens":120650,"cache_creation_input_tokens":13908,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":13908},"type":"message"}]},"context_management":{"applied_edits":[]}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"50843da5-a0f0-4e95-a344-f841aa6d9499"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_stop"},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"f5993155-901f-4e2d-a3a9-4d5b23c1bbf5"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CqVNFCESeShHVUiLMLPsCg","type":"tool_result","content":"{\"markdown\":\"Thisdomainisforuseindocumentationexampleswithoutneedingpermission.Thisisnotaservice,avoidrelyingonitfortestingandmonitoringpurposes.\\n\\nهذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إلى إذن. هذه ليست خدمة، يُرجى تجنب الاعتماد عليها لأغراض الاختبار والمراقبة.\\n\\n该域名仅用于文档示例,无需获得许可。这并非一项服务,请勿将其用于测试和监控目的。\\n\\nL’usagedecedomaineestréservéàdesexemplesdedocumentation,sansautorisationpréalable.Ilnes’agitpasd’unservice;sonutilisationàdesfinsdetestoudesurveillanceestàéviter.\\n\\nДанныйдоменпредназначендляиспользованиявпримерахдокументациибезнеобходимостиполученияпредварительногоразрешения.Этонесервис;нерекомендуетсяегоиспользованиедлятестированияимониторинга.\\n\\nEstedominioestádestinadoalusoenejemplosdedocumentaciónsinnecesidaddepermiso.Estonoesunservicio,evitarutilizarlopararealizarpruebasomonitoreos.\\n\\n [Learn more](https://iana.org/help/example-domains)\",\"metadata\":{\"viewport\":\"width=device-width,initial-scale=1\",\"title\":\"Example Domain\",\"language\":\"en\",\"favicon\":\"data:,\",\"scrapeId\":\"01a0edcb-958c-76df-93e9-f9d9dbd6ae55\",\"sourceURL\":\"https://example.com\",\"url\":\"https://example.com/\",\"statusCode\":200,\"contentType\":\"text/html; charset=utf-8\",\"proxyUsed\":\"basic\",\"cacheState\":\"hit\",\"cachedAt\":\"2026-09-29T15:32:11.421Z\",\"creditsUsed\":1,\"concurrencyLimited\":false}}"}]},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"863025d4-d60a-4ea5-95e5-7fb64a0b386a","timestamp":"2026-09-29T15:32:17.556Z","tool_use_result":{"content":"{\"markdown\":\"Thisdomainisforuseindocumentationexampleswithoutneedingpermission.Thisisnotaservice,avoidrelyingonitfortestingandmonitoringpurposes.\\n\\nهذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إلى إذن. هذه ليست خدمة، يُرجى تجنب الاعتماد عليها لأغراض الاختبار والمراقبة.\\n\\n该域名仅用于文档示例,无需获得许可。这并非一项服务,请勿将其用于测试和监控目的。\\n\\nL’usagedecedomaineestréservéàdesexemplesdedocumentation,sansautorisationpréalable.Ilnes’agitpasd’unservice;sonutilisationàdesfinsdetestoudesurveillanceestàéviter.\\n\\nДанныйдоменпредназначендляиспользованиявпримерахдокументациибезнеобходимостиполученияпредварительногоразрешения.Этонесервис;нерекомендуетсяегоиспользованиедлятестированияимониторинга.\\n\\nEstedominioestádestinadoalusoenejemplosdedocumentaciónsinnecesidaddepermiso.Estonoesunservicio,evitarutilizarlopararealizarpruebasomonitoreos.\\n\\n [Learn more](https://iana.org/help/example-domains)\",\"metadata\":{\"viewport\":\"width=device-width,initial-scale=1\",\"title\":\"Example Domain\",\"language\":\"en\",\"favicon\":\"data:,\",\"scrapeId\":\"01a0edcb-958c-76df-93e9-f9d9dbd6ae55\",\"sourceURL\":\"https://example.com\",\"url\":\"https://example.com/\",\"statusCode\":200,\"contentType\":\"text/html; charset=utf-8\",\"proxyUsed\":\"basic\",\"cacheState\":\"hit\",\"cachedAt\":\"2026-09-29T15:32:11.421Z\",\"creditsUsed\":1,\"concurrencyLimited\":false}}","structuredContent":{"markdown":"Thisdomainisforuseindocumentationexampleswithoutneedingpermission.Thisisnotaservice,avoidrelyingonitfortestingandmonitoringpurposes.\n\nهذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إلى إذن. هذه ليست خدمة، يُرجى تجنب الاعتماد عليها لأغراض الاختبار والمراقبة.\n\n该域名仅用于文档示例,无需获得许可。这并非一项服务,请勿将其用于测试和监控目的。\n\nL’usagedecedomaineestréservéàdesexemplesdedocumentation,sansautorisationpréalable.Ilnes’agitpasd’unservice;sonutilisationàdesfinsdetestoudesurveillanceestàéviter.\n\nДанныйдоменпредназначендляиспользованиявпримерахдокументациибезнеобходимостиполученияпредварительногоразрешения.Этонесервис;нерекомендуетсяегоиспользованиедлятестированияимониторинга.\n\nEstedominioestádestinadoalusoenejemplosdedocumentaciónsinnecesidaddepermiso.Estonoesunservicio,evitarutilizarlopararealizarpruebasomonitoreos.\n\n [Learn more](https://iana.org/help/example-domains)","metadata":{"viewport":"width=device-width,initial-scale=1","title":"Example Domain","language":"en","favicon":"data:,","scrapeId":"01a0edcb-958c-76df-93e9-f9d9dbd6ae55","sourceURL":"https://example.com","url":"https://example.com/","statusCode":200,"contentType":"text/html; charset=utf-8","proxyUsed":"basic","cacheState":"hit","cachedAt":"2026-09-29T15:32:11.421Z","creditsUsed":1,"concurrencyLimited":false}}}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"status","status":"requesting","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"cece3d42-d089-4279-89b6-cc1ecfcfa1de"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyMiXtkRS4bQYoe7LVs","type":"message","role":"assistant","content":[],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":787,"cache_read_input_tokens":134558,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":787},"output_tokens":37,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"dc3467a0-7e44-4bbc-a952-84aeae32d2c9","ttft_ms":2543}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_019X6ALn5NrExXqfhzC3LHCP","name":"mcp__claude_ai_Firecrawl__firecrawl_map","input":{},"caller":{"type":"direct"}}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"ade642dc-47a3-4cc8-ab74-a78d1ddb4917"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"3b9a9dc0-2368-4ea0-8bdb-99b40333405a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"url\":"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"30cfa68f-ba26-49b4-817c-2360cb007e55"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":" \"https"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"b5c6bb6f-2e1b-4eeb-9cd6-8c715de7dec2"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"://exam"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"106034cd-4582-48b1-9ac7-5fd3334e93d2"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"ple.com"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"80eda895-c023-415c-9393-c58d70e82b38"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"36249eb1-accf-414b-88f1-b7f2f55c68a2"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":", \"limi"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"21b45389-6404-4e93-8e1b-700e996bfbb5"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"t\": 1}"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"493d77b0-2e83-47fe-a953-c6e7cb2eda59"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyMiXtkRS4bQYoe7LVs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019X6ALn5NrExXqfhzC3LHCP","name":"mcp__claude_ai_Firecrawl__firecrawl_map","input":{"url":"https://example.com","limit":1},"caller":{"type":"direct"}}],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":787,"cache_read_input_tokens":134558,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":787},"output_tokens":37,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"0b290b49-0022-43e3-a828-aedb55fdd349","timestamp":"2026-09-29T15:32:20.649Z","wire_tool_inputs":{"toolu_019X6ALn5NrExXqfhzC3LHCP":{"url":"https://example.com","limit":1}}}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_stop","index":0},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"864fb881-63f5-4b90-9c77-1e725ab597a4"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"container":null},"usage":{"input_tokens":1,"cache_creation_input_tokens":787,"cache_read_input_tokens":134558,"output_tokens":90,"output_tokens_details":{"thinking_tokens":0},"iterations":[{"input_tokens":1,"output_tokens":90,"cache_read_input_tokens":134558,"cache_creation_input_tokens":787,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":787},"type":"message"}]},"context_management":{"applied_edits":[]}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"f89382a7-88b9-4b7c-8a34-844fd01020c0"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_stop"},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"9557dc84-46de-4ae5-8d21-c75b8d931ddb"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019X6ALn5NrExXqfhzC3LHCP","type":"tool_result","content":"{\"links\":[{\"url\":\"https://example.com/\",\"title\":\"Example Domain\"}],\"id\":\"01a0edcb-a3ec-762d-b334-eac9eaf6e0e2\"}"}]},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"e11ebe37-f6cc-4566-b1d3-1ef09fe3b555","timestamp":"2026-09-29T15:32:21.366Z","tool_use_result":{"content":"{\"links\":[{\"url\":\"https://example.com/\",\"title\":\"Example Domain\"}],\"id\":\"01a0edcb-a3ec-762d-b334-eac9eaf6e0e2\"}","structuredContent":{"links":[{"url":"https://example.com/","title":"Example Domain"}],"id":"01a0edcb-a3ec-762d-b334-eac9eaf6e0e2"}}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"status","status":"requesting","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"5bc88c9e-c3a2-4f4c-9627-526f1328149d"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyN16JbrX8sT6DrKgDY","type":"message","role":"assistant","content":[],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":153,"cache_read_input_tokens":135345,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":153},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"29cd9f10-713b-4fa5-b078-cf002afa0f36","ttft_ms":1348}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"3d567d6d-5900-46b1-93fc-d7fa3af9a497"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"m"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"c0fe9b4c-25ad-4b78-9ec7-46ee5ec7902c"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"cp presentation fixture complete"}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"2d9455f8-b3ae-4745-975a-364397a78a48"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011CfXyN16JbrX8sT6DrKgDY","type":"message","role":"assistant","content":[{"type":"text","text":"mcp presentation fixture complete"}],"container":null,"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":153,"cache_read_input_tokens":135345,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":153},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","uuid":"9e83bfdd-927a-4434-bc3e-7065c7fd4198","timestamp":"2026-09-29T15:32:22.734Z"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"content_block_stop","index":0},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"ba30c145-2f37-42a3-a72c-88bfb16d1b63"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"container":null},"usage":{"input_tokens":1,"cache_creation_input_tokens":153,"cache_read_input_tokens":135345,"output_tokens":8,"output_tokens_details":{"thinking_tokens":0},"iterations":[{"input_tokens":1,"output_tokens":8,"cache_read_input_tokens":135345,"cache_creation_input_tokens":153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":153},"type":"message"}]},"context_management":{"applied_edits":[]}},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"4275a6ef-1a6a-488d-82ab-5c5f04ef272a"}} +{"type":"emit_inbound","label":"stream_event","frame":{"type":"stream_event","event":{"type":"message_stop"},"session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","parent_tool_use_id":null,"uuid":"c82d8732-2755-4c3c-8447-ece5d38ee460"}} +{"type":"emit_inbound","label":"result","frame":{"duration_api_ms":8468,"stop_reason":"end_turn","session_id":"e51402d3-25be-4866-81d5-2ab30cecb268","total_cost_usd":0.2111049,"usage":{"input_tokens":5,"cache_creation_input_tokens":14848,"cache_read_input_tokens":390553,"output_tokens":289,"output_tokens_details":{"thinking_tokens":95},"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":14848,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":1,"output_tokens":8,"cache_read_input_tokens":135345,"cache_creation_input_tokens":153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":153},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":8,"outputTokens":300,"cacheReadInputTokens":391643,"cacheCreationInputTokens":14848,"webSearchRequests":0,"costUSD":0.2111049,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":95,"canonicalModel":"claude-sonnet-4-6","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":false,"num_turns":3,"subtype":"success","api_error_status":null,"result":"mcp presentation fixture complete","ttft_ms":2932,"type":"result","duration_ms":8858,"uuid":"4561c900-5246-467b-8b6a-b16f2a116f64","ttft_stream_ms":1429,"time_to_request_ms":31,"first_content_frame_ms":1429,"user_message_uuid":"8935142f-0551-46e6-9a6b-95034332fa1a","user_message_uuids":["8935142f-0551-46e6-9a6b-95034332fa1a"],"request_sent_wall_ms":1790695934024,"queued_turn_count":0,"result_index":0}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts new file mode 100644 index 000000000000..5c0796108be2 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts @@ -0,0 +1,14 @@ +import type { OrchestratorFixtureInput } from "../shared.ts"; + +export const CLAUDE_MCP_TOOL_PRESENTATION_PROMPT = [ + "Do exactly this, one tool call at a time, with no other tool calls:", + '1) Call the Firecrawl firecrawl_scrape tool on https://example.com with formats ["markdown"].', + "2) Call the Firecrawl firecrawl_map tool on https://example.com with limit 1.", + "3) Reply with exactly: mcp presentation fixture complete", +].join("\n"); + +export function claudeMcpToolPresentationInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: CLAUDE_MCP_TOOL_PRESENTATION_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts new file mode 100644 index 000000000000..c68b57d83a8c --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts @@ -0,0 +1,51 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, +} from "../shared.ts"; +import { CLAUDE_MCP_TOOL_PRESENTATION_PROMPT } from "./input.ts"; + +// The scrape frame carries Claude Code's `tool_use_meta` as recorded. The map +// frame's entry was removed from the recording, so it stands in for an MCP +// tool Claude Code sends no display metadata for. +export function assertClaudeMcpToolPresentationOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertUserMessagesInclude(projection, [CLAUDE_MCP_TOOL_PRESENTATION_PROMPT]); + + const tools = projection.turnItems.flatMap((item) => + item.type === "dynamic_tool" ? [item] : [], + ); + assert.deepEqual( + tools.map((item) => item.toolName), + ["mcp__claude_ai_Firecrawl__firecrawl_scrape", "mcp__claude_ai_Firecrawl__firecrawl_map"], + ); + const [scrape, map] = tools; + + assert.equal(scrape?.status, "completed"); + assert.equal(scrape?.title, "Firecrawl scrape"); + assert.deepEqual(scrape?.toolSource, { + key: "mcp:firecrawl", + name: "Firecrawl", + kind: "integration", + icon: { + _tag: "themed-logo", + logoUrl: "https://www.google.com/s2/favicons?domain=firecrawl.dev&sz=64", + }, + }); + + assert.equal(map?.status, "completed"); + assert.isNull(map?.title); + assert.notProperty(map, "toolSource"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/index.ts b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts index 1694692faa03..ca647d194d7e 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/index.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts @@ -20,6 +20,8 @@ import { claudeIdleResumeInput } from "./claude_idle_resume/input.ts"; import { assertClaudeIdleResumeOutput } from "./claude_idle_resume/output.ts"; import { claudeLocalBashTaskInput } from "./claude_local_bash_task/input.ts"; import { assertClaudeLocalBashTaskOutput } from "./claude_local_bash_task/output.ts"; +import { claudeMcpToolPresentationInput } from "./claude_mcp_tool_presentation/input.ts"; +import { assertClaudeMcpToolPresentationOutput } from "./claude_mcp_tool_presentation/output.ts"; import { claudeResultIsErrorInput } from "./claude_result_is_error/input.ts"; import { assertClaudeResultIsErrorOutput } from "./claude_result_is_error/output.ts"; import { grokAutoBlockedCommandInput } from "./grok_auto_blocked_command/input.ts"; @@ -292,6 +294,21 @@ export const ORCHESTRATOR_REPLAY_FIXTURES: ReadonlyArray Date: Tue, 29 Sep 2026 12:08:05 -0400 Subject: [PATCH 2/5] fix(server): keep Claude tool presentation helpers module-private Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index a8a60badc263..0dffe5e3ac35 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -2032,7 +2032,7 @@ function claudeToolUseBlocksFromAssistantMessage( return message.message.content.filter(isClaudeToolUseContentBlock); } -export interface ClaudeToolPresentation { +interface ClaudeToolPresentation { readonly title: string; readonly toolSource?: ToolActivitySource; } @@ -2058,7 +2058,7 @@ function metaHttpUrl(value: unknown): string | undefined { * humanized name) plus the server's name and icon. The CLI sends these as * `tool_use_meta` on assistant frames; the SDK types do not declare it yet. */ -export function claudeToolPresentationsFromAssistantMessage( +function claudeToolPresentationsFromAssistantMessage( message: SDKMessage, ): ReadonlyMap { const presentations = new Map(); From 0cdbf641ea5ea4eb86da6cf936bcf0127d416524 Mon Sep 17 00:00:00 2001 From: scratchyone <11479077+scratchyone@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:05 -0400 Subject: [PATCH 3/5] fix(server): bound Claude MCP icon URLs after normalization Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 0dffe5e3ac35..a05eae0b14e3 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -2047,7 +2047,9 @@ function metaHttpUrl(value: unknown): string | undefined { if (typeof value !== "string" || value.length > 4096) return undefined; try { const url = new URL(value); - return url.protocol === "https:" || url.protocol === "http:" ? url.href : undefined; + return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096 + ? url.href + : undefined; } catch { return undefined; } From 4bb02e09d24366576a629f9e74cccfa40112e930 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:40:50 +0200 Subject: [PATCH 4/5] fix(server): share MCP tool presentation across providers --- .../Adapters/AcpAdapterV2.test.ts | 23 ++++ .../orchestration-v2/Adapters/AcpAdapterV2.ts | 19 ++- .../Adapters/ClaudeAdapterV2.test.ts | 101 +++++++++++++++ .../Adapters/ClaudeAdapterV2.ts | 73 ++++------- .../Adapters/CodexAdapterV2.test.ts | 42 +++++- .../Adapters/CodexAdapterV2.ts | 5 +- .../Adapters/CursorAdapterV2.test.ts | 40 ++++++ .../Adapters/CursorAdapterV2.ts | 7 + .../Adapters/OpenCodeAdapterV2.test.ts | 121 ++++++++++++++++++ .../Adapters/OpenCodeAdapterV2.testkit.ts | 1 + .../Adapters/OpenCodeAdapterV2.ts | 27 +++- .../Adapters/PiAdapterV2.test.ts | 41 ++++++ .../orchestration-v2/Adapters/PiAdapterV2.ts | 2 + .../claude_mcp_tool_presentation/output.ts | 8 +- .../src/provider/CodexToolPresentation.ts | 35 +++-- .../src/provider/McpToolPresentation.test.ts | 65 ++++++++++ .../src/provider/McpToolPresentation.ts | 62 +++++++++ .../src/provider/acp/AcpRuntimeModel.test.ts | 31 ++++- .../src/provider/acp/AcpRuntimeModel.ts | 20 ++- 19 files changed, 631 insertions(+), 92 deletions(-) create mode 100644 apps/server/src/provider/McpToolPresentation.test.ts create mode 100644 apps/server/src/provider/McpToolPresentation.ts diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 562fc5cdf21f..27c7380d7f26 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1075,6 +1075,17 @@ describe("AcpAdapterV2", () => { "cognition.ai/subagent_context": { parentAgentId: "child-a" }, }, }, + { + sessionUpdate: "tool_call", + toolCallId: "child-weather", + title: "Check weather", + status: "completed", + rawInput: { server: "weather", tool: "get_weather", city: "Berlin" }, + _meta: { + is_mcp_tool_call: true, + "cognition.ai/subagent_context": { parentAgentId: "child-a" }, + }, + }, { sessionUpdate: "agent_message_chunk", content: { type: "text", text: " → decider → event)." }, @@ -1192,6 +1203,18 @@ describe("AcpAdapterV2", () => { ); assert.deepEqual([...childMessages.values()], ["Checking the code.", "ONE"]); assert.equal(task?.prompt, "Run pwd, then reply ONE."); + const childMcp = items.find( + (item) => + item.threadId === task?.childThreadId && + item.type === "dynamic_tool" && + item.toolName === "weather.get_weather", + ); + assert.equal(childMcp?.title, "get weather"); + assert.deepEqual(childMcp?.toolSource, { + key: "mcp:weather", + name: "weather", + kind: "integration", + }); assert.isTrue( items.some( (item) => diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts index 938144e98062..90dd098ad503 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts @@ -62,6 +62,7 @@ import { makeAcpMcpOverAcpBridge, type AcpMcpOverAcpBridge, } from "../../mcp/AcpMcpOverAcpBridge.ts"; +import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { applyAcpAgentTerminalUpdate, @@ -3258,9 +3259,11 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV } else if (mcpIdentity !== undefined) { turnItem = { ...base, - // Identity lives in toolName, like native Codex MCP items; the - // agent's own title (e.g. "Ran command") would shadow it. title: null, + ...mcpToolPresentation({ + serverName: mcpIdentity.server, + toolName: mcpIdentity.tool, + }), type: "dynamic_tool", toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`, input: @@ -4285,6 +4288,7 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV const key = `${nativeTaskId}:tool:${toolCall.toolCallId}`; const merged = mergeToolCallState(context.tools.get(key), toolCall); context.tools.set(key, merged); + const mcpIdentity = extractMcpToolCallIdentity(merged); const now = yield* DateTime.now; const status = toolStatus(merged.status); const startedAt = context.toolStartedAt.get(key) ?? now; @@ -4309,7 +4313,16 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV completedAt: completedAtForStatus(status, now), updatedAt: now, type: "dynamic_tool", - toolName: merged.title ?? merged.kind ?? "Tool", + ...(mcpIdentity === undefined + ? {} + : mcpToolPresentation({ + serverName: mcpIdentity.server, + toolName: mcpIdentity.tool, + })), + toolName: + mcpIdentity === undefined + ? (merged.title ?? merged.kind ?? "Tool") + : `${mcpIdentity.server}.${mcpIdentity.tool}`, input: merged.data.rawInput ?? null, output: merged.data.rawOutput ?? merged.data.content ?? null, }, diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 2ef8fcc0d7c4..c491bcc641a4 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -2132,6 +2132,107 @@ describe("ClaudeAdapterV2 background wake turns", () => { }); const makeWakeHarness = makeWakeHarnessWithOptions(); + it.effect.each([ + { isError: false, title: "Check weather" }, + { isError: true, title: "Check weather" }, + { isError: false, title: undefined }, + ])("keeps late MCP display metadata with %j", ({ isError, title }) => + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("mcp-presentation"), + text: "Check the weather", + attachments: [], + }), + ); + const toolName = "mcp__weather__get_weather"; + const id = "weather-call"; + yield* Effect.promise(() => + harness.getOpenedOptions()!.canUseTool!( + toolName, + { city: "Berlin" }, + { + signal: new AbortController().signal, + toolUseID: id, + requestId: "weather-request", + }, + ), + ); + yield* harness.offerAndWait( + claudeSdkFrame({ + type: "assistant", + uuid: "weather-assistant", + session_id: WAKE_NATIVE_SESSION, + parent_tool_use_id: null, + message: { + id: "weather-message", + type: "message", + role: "assistant", + model: "claude-sonnet-4-6", + content: [{ type: "tool_use", id, name: toolName, input: { city: "Berlin" } }], + }, + tool_use_meta: [ + { + id, + display_name: title, + server_display_name: "Weather", + icon_url: "https://example.com/weather.png", + }, + ], + }), + ); + yield* harness.offerAndWait( + claudeSdkFrame({ + type: "user", + uuid: "weather-result", + session_id: WAKE_NATIVE_SESSION, + parent_tool_use_id: null, + message: { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: id, + content: "Weather result", + is_error: isError, + }, + ], + }, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + makeResultFrame({ uuid: "weather-terminal", result: "Weather checked" }), + ); + yield* Queue.take(harness.terminalReceipts); + const items = harness.events.flatMap((event) => + event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool" + ? [event.turnItem] + : [], + ); + assert.deepEqual( + items.map((item) => item.status), + ["running", "running", isError ? "failed" : "completed"], + ); + assert.isNull(items[0]?.title); + for (const item of items.slice(1)) { + assert.equal(item.title, title ?? "get weather"); + assert.deepEqual(item.toolSource, { + key: "mcp:weather", + name: "Weather", + kind: "integration", + icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" }, + }); + assert.deepEqual(item.input, { city: "Berlin" }); + } + assert.equal(new Set(items.map((item) => item.id)).size, 1); + }).pipe(Effect.scoped, Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer))), + ); + it.effect.each(["completed", "interrupted"] as const)( "projects Claude thinking blocks when %s", (status) => diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index a05eae0b14e3..968f8d4e9958 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -63,7 +63,6 @@ import { type ProviderUserInputAnswers, type ProviderThreadId, type ThreadId, - type ToolActivitySource, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; @@ -111,6 +110,7 @@ import type { ServerProviderShape } from "../../provider/Services/ServerProvider import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts"; import { buildRuntimeInstructions } from "../../provider/RuntimeInstructions.ts"; +import { mcpToolPresentation, normalizeMcpText } from "../../provider/McpToolPresentation.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; import { makeProviderFailure, makeProviderRetryTurnItem } from "../ProviderFailure.ts"; @@ -2032,62 +2032,33 @@ function claudeToolUseBlocksFromAssistantMessage( return message.message.content.filter(isClaudeToolUseContentBlock); } -interface ClaudeToolPresentation { - readonly title: string; - readonly toolSource?: ToolActivitySource; -} - -function boundedMetaText(value: unknown, maxLength: number): string | undefined { - if (typeof value !== "string") return undefined; - const text = value.trim().replace(/\s+/gu, " "); - return text.length > 0 && text.length <= maxLength ? text : undefined; -} - -function metaHttpUrl(value: unknown): string | undefined { - if (typeof value !== "string" || value.length > 4096) return undefined; - try { - const url = new URL(value); - return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096 - ? url.href - : undefined; - } catch { - return undefined; - } -} +type ClaudeToolPresentation = ReturnType; -/** - * Claude Code's own label for each tool use (the MCP tool's title, or its - * humanized name) plus the server's name and icon. The CLI sends these as - * `tool_use_meta` on assistant frames; the SDK types do not declare it yet. - */ function claudeToolPresentationsFromAssistantMessage( message: SDKMessage, ): ReadonlyMap { const presentations = new Map(); const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined; if (!Array.isArray(meta)) return presentations; + const toolNames = new Map( + claudeToolUseBlocksFromAssistantMessage(message).map((tool) => [ + tool.id, + tool.name.replace(/^mcp__claude_ai_/u, "mcp__"), + ]), + ); for (const entry of meta) { if (typeof entry !== "object" || entry === null) continue; - const id = boundedMetaText(Reflect.get(entry, "id"), 512); - const title = boundedMetaText(Reflect.get(entry, "display_name"), 160); - if (id === undefined || title === undefined) continue; - const serverName = boundedMetaText(Reflect.get(entry, "server_display_name"), 160); - const iconUrl = metaHttpUrl(Reflect.get(entry, "icon_url")); - presentations.set(id, { - title, - ...(serverName === undefined - ? {} - : { - toolSource: { - key: `mcp:${serverName.toLowerCase()}`, - name: serverName, - kind: "integration", - ...(iconUrl === undefined - ? {} - : { icon: { _tag: "themed-logo", logoUrl: iconUrl } as const }), - }, - }), - }); + const id = normalizeMcpText(Reflect.get(entry, "id"), 512); + if (id === undefined) continue; + presentations.set( + id, + mcpToolPresentation({ + toolName: toolNames.get(id), + title: Reflect.get(entry, "display_name"), + serverDisplayName: Reflect.get(entry, "server_display_name"), + iconUrl: Reflect.get(entry, "icon_url"), + }), + ); } return presentations; } @@ -5847,7 +5818,11 @@ export function makeClaudeAdapterV2( toolName: toolUse.name, toolInput: nativeToolInput, parentToolUseId: parentToolUseIdFromSdkMessage(message), - presentation: toolPresentations.get(toolUse.id), + presentation: + toolPresentations.get(toolUse.id) ?? + mcpToolPresentation({ + toolName: toolUse.name.replace(/^mcp__claude_ai_/u, "mcp__"), + }), }); const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id); if (heldPlan !== undefined) { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index dc82807c76e7..7cc64cb2623c 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -810,11 +810,43 @@ describe("CodexAdapterV2 dynamic tool projection", () => { result: { content: [] }, }; assert.equal(projectCodexDynamicToolItem(call).title, "Inspect Saga music screen"); - assert.equal( - projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title, - undefined, - ); - assert.equal(projectCodexDynamicToolItem({ ...call, server: "github" }).title, undefined); + assert.equal(projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title, "js"); + assert.equal(projectCodexDynamicToolItem({ ...call, server: "github" }).title, "js"); + }); + + it.each(["inProgress", "completed", "failed"] as const)( + "presents ordinary MCP calls when %s", + (status) => { + const projection = projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "weather-call", + server: "weather", + tool: "get_weather", + status, + arguments: { city: "Berlin" }, + }); + assert.equal(projection.title, "get weather"); + assert.deepEqual(projection.toolSource, { + key: "mcp:weather", + name: "weather", + kind: "integration", + }); + assert.deepEqual(projection.input, { city: "Berlin" }); + }, + ); + + it("uses Codex connector names without reading a display title from arguments", () => { + const projection = projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "connector-call", + server: "_apps", + tool: "connector_get_weather", + status: "completed", + arguments: { title: "Argument, not display metadata" }, + appContext: { connectorId: "weather-app", appName: "Weather", actionName: "Check weather" }, + }); + assert.equal(projection.title, "Check weather"); + assert.equal(projection.toolSource?.name, "Weather"); }); it("preserves native browser and app icons alongside MCP tool output", () => { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index daff82fadbb8..450c57b7e9ce 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -491,9 +491,10 @@ export function projectCodexDynamicToolItem( item.type === "mcpToolCall" ? `${item.server}.${item.tool}` : [trimText(item.namespace), item.tool].filter(Boolean).join("."); - const title = computerUseToolTitle(toolName, item.arguments); + const presentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}; + const title = computerUseToolTitle(toolName, item.arguments) ?? presentation.title; const projection: CodexDynamicToolProjection = { - ...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}), + ...presentation, toolName, ...(title ? { title } : {}), input: item.arguments, diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts index 7d51f3793a7e..a0ffd917db73 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -359,6 +359,22 @@ describe("CursorAdapterV2", () => { numFiles: 0, }; const updates: ReadonlyArray = [ + ...(["tool-call-started", "tool-call-completed"] as const).map((type) => ({ + type, + modelCallId: "native-model-call", + callId: "mcp-weather", + toolCall: { + type: "mcp" as const, + args: { + providerIdentifier: "weather", + toolName: "get_weather", + args: { city: "Berlin" }, + }, + ...(type === "tool-call-completed" + ? { result: { status: "success" as const, value: { content: [], isError: false } } } + : {}), + }, + })), { type: "tool-call-completed", modelCallId: "native-model-call", @@ -605,6 +621,30 @@ describe("CursorAdapterV2", () => { Stream.takeUntil((event) => event.type === "turn.terminal"), Stream.runCollect, ); + const mcpItems = events.flatMap((event) => + event.type === "turn_item.updated" && + event.turnItem.type === "dynamic_tool" && + event.turnItem.toolName === "mcp__weather__get_weather" + ? [event.turnItem] + : [], + ); + assert.deepEqual( + mcpItems.map((item) => item.status), + ["running", "completed"], + ); + for (const item of mcpItems) { + assert.equal(item.title, "get weather"); + assert.deepEqual(item.toolSource, { + key: "mcp:weather", + name: "weather", + kind: "integration", + }); + assert.deepEqual(item.input, { + providerIdentifier: "weather", + toolName: "get_weather", + args: { city: "Berlin" }, + }); + } const fileSearchItems = events.flatMap((event) => event.type === "turn_item.updated" && event.turnItem.type === "file_search" && diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts index 4829fb0cb3c7..22f0f2d0acd7 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -44,6 +44,7 @@ import * as Stream from "effect/Stream"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; +import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { CursorTransportFailure } from "../../provider/acp/CursorTransportFailure.ts"; import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts"; @@ -1294,6 +1295,12 @@ export function makeCursorAdapterV2( turnItem = { ...base, type: "dynamic_tool", + ...(toolCall.type === "mcp" + ? mcpToolPresentation({ + serverName: toolCall.args.providerIdentifier, + toolName: toolCall.args.toolName, + }) + : {}), toolName: cursorToolName(toolCall), input: toolCall.args, ...(cursorToolOutput(toolCall) === undefined diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts index fa32da8c4637..79e26e86644f 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts @@ -884,6 +884,127 @@ describe("OpenCodeAdapterV2", () => { }).pipe(Effect.provide(idAllocatorLayer), Effect.scoped), ); + it.effect("presents OpenCode MCP calls without treating remote tools as local edits", () => + Effect.gen(function* () { + const nativeEvents = asyncEventStream(); + const nativeSessionId = "native-opencode-mcp"; + let statusReads = 0; + const harness = yield* makeOpenCodeRuntimeHarness("mcp-presentation", nativeSessionId, { + event: { + subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => { + options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true }); + return { stream: nativeEvents.stream }; + }, + }, + session: { + create: async () => ({ data: { id: nativeSessionId, time: { created: 1, updated: 1 } } }), + promptAsync: async () => ({ data: true }), + }, + mcp: { + status: async () => { + statusReads++; + return { + data: { + "my.server_with_underscores": { status: "connected" }, + ambiguous: { status: "connected" }, + ambiguous_server: { status: "connected" }, + }, + }; + }, + }, + }); + yield* harness.startTurn(); + const received = yield* harness.runtime.events.pipe( + Stream.takeUntil( + (event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction", + ), + Stream.runCollect, + Effect.forkScoped, + ); + for (const status of ["running", "completed", "error"] as const) { + yield* Effect.promise(() => + nativeEvents.push({ + type: "message.part.updated", + properties: { + sessionID: nativeSessionId, + part: { + id: "mcp-part", + sessionID: nativeSessionId, + messageID: "mcp-assistant", + type: "tool", + callID: "mcp-call", + tool: "my_server_with_underscores_edit_document", + state: { + status, + input: { document: "remote-doc" }, + title: "Edit remote document", + metadata: {}, + output: "saved", + error: "failed", + time: { start: 1, end: 2 }, + }, + }, + }, + }), + ); + } + yield* Effect.promise(() => + nativeEvents.push({ + type: "message.part.updated", + properties: { + sessionID: nativeSessionId, + part: { + id: "ambiguous-part", + sessionID: nativeSessionId, + messageID: "mcp-assistant", + type: "tool", + callID: "ambiguous-call", + tool: "ambiguous_server_edit_document", + state: { + status: "completed", + input: {}, + title: "Ambiguous tool", + metadata: {}, + output: "", + time: { start: 1, end: 2 }, + }, + }, + }, + }), + ); + yield* Effect.promise(() => + nativeEvents.push({ + type: "session.compacted", + properties: { sessionID: nativeSessionId }, + }), + ); + const items = (yield* Fiber.join(received)).flatMap((event) => + event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool" + ? [event.turnItem] + : [], + ); + assert.deepEqual( + items.slice(0, 3).map((item) => item.status), + ["running", "completed", "failed"], + ); + assert.deepEqual( + items.slice(0, 3).map((item) => item.title), + ["Edit remote document", "Edit remote document", "edit document"], + ); + for (const item of items.slice(0, 3)) { + assert.deepEqual(item.toolSource, { + key: "mcp:my.server_with_underscores", + name: "my.server with underscores", + kind: "integration", + }); + assert.deepEqual(item.input, { document: "remote-doc" }); + } + assert.equal(items[3]?.toolName, "ambiguous_server_edit_document"); + assert.isUndefined(items[3]?.toolSource); + assert.equal(statusReads, 1); + }).pipe(Effect.provide(idAllocatorLayer), Effect.scoped), + ); + it.effect("admits a native command on its user receipt before generation completes", () => Effect.gen(function* () { const nativeEvents = asyncEventStream(); diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts index 74be69433fbe..1aedfd0b0cd5 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts @@ -334,6 +334,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient reply: (input: unknown) => request("question.reply", input), }, mcp: { + status: () => request("mcp.status", {}), add: (input: unknown) => request("mcp.add", input), }, } as unknown as OpencodeClient; diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts index 0b781b5f82cf..44421b64b0c0 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts @@ -57,6 +57,7 @@ import * as Stream from "effect/Stream"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; +import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; import { ProviderEventLoggers } from "../../provider/Layers/ProviderEventLoggers.ts"; @@ -315,6 +316,7 @@ interface ActiveOpenCodeTurn { readonly parts: Map>; readonly partIdsByMessage: Map>; readonly toolNamesByCallId: Map; + mcpServerNames?: ReadonlyArray; readonly providerTurn: OrchestrationV2ProviderTurn; nextItemOrdinal: number; nativeUserMessageId: string | null; @@ -1666,7 +1668,29 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid >; const input = toolInput(part); const output = toolOutput(part); - const projectionKind = openCodeToolProjectionKind(part.tool); + if (part.tool.includes("_") && turn.mcpServerNames === undefined) { + turn.mcpServerNames = yield* runOpenCodeSdk("mcp.status", () => + client.mcp.status(), + ).pipe( + Effect.map((response) => Object.keys(response.data ?? {})), + Effect.catch(() => Effect.succeed([])), + ); + } + const matchingServers = turn.mcpServerNames?.filter((name) => + part.tool.startsWith(`${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_`), + ); + const serverName = matchingServers?.length === 1 ? matchingServers[0] : undefined; + const presentation = + serverName === undefined + ? {} + : mcpToolPresentation({ + serverName, + toolName: part.tool.slice(serverName.replace(/[^a-zA-Z0-9_-]/g, "_").length + 1), + title: toolTitle(part) === part.tool ? undefined : toolTitle(part), + }); + const projectionKind = matchingServers?.length + ? "dynamic_tool" + : openCodeToolProjectionKind(part.tool); let turnItem: OrchestrationV2TurnItem; if (projectionKind === "command_execution") { turnItem = { @@ -1739,6 +1763,7 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid ? formatReadToolLabel(readPath) : base.title, type: "dynamic_tool", + ...presentation, toolName: part.tool, input, ...(output === undefined ? {} : { output }), diff --git a/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts index b82dad9078ed..2faec00197aa 100644 --- a/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts @@ -1068,6 +1068,47 @@ describe("PiAdapterV2", () => { ); } + it.effect("presents explicitly namespaced MCP extension tools", () => + Effect.gen(function* () { + const fake = yield* makeFakePi; + const { runtime, takeEvent } = yield* openRuntime(fake); + const providerThread = yield* runtime.ensureThread({ + threadId: THREAD_ID, + modelSelection: modelSelection("default"), + runtimePolicy, + }); + yield* startTurn(runtime, providerThread); + yield* fake.takeRequest("prompt"); + yield* fake.emit({ type: "agent_start" }); + for (const type of ["tool_execution_start", "tool_execution_end"]) { + yield* fake.emit({ + type, + toolCallId: "weather-call", + toolName: "mcp__weather__get_weather", + args: { city: "Berlin" }, + result: { content: [{ type: "text", text: "Sunny" }] }, + isError: false, + }); + const event = yield* takeEvent( + (event) => event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool", + ); + if (event.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool") + return yield* Effect.die("Expected an MCP tool item"); + assert.equal(event.turnItem.title, "get weather"); + assert.equal( + event.turnItem.status, + type === "tool_execution_start" ? "running" : "completed", + ); + assert.deepEqual(event.turnItem.toolSource, { + key: "mcp:weather", + name: "weather", + kind: "integration", + }); + assert.deepEqual(event.turnItem.input, { city: "Berlin" }); + } + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + it.effect("observes official subagent results without inventing child threads", () => Effect.gen(function* () { const fake = yield* makeFakePi; diff --git a/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts index dc19647fa583..bac10072ffdc 100644 --- a/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts @@ -61,6 +61,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; +import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { expandPiSkillReference, @@ -1029,6 +1030,7 @@ export function makePiAdapterV2(options: PiAdapterV2Options): ProviderAdapterV2S ...shared, title: toolName, type: "dynamic_tool", + ...mcpToolPresentation({ toolName }), toolName, input: args ?? {}, ...(outputText.length > 0 ? { output: outputText } : {}), diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts index c68b57d83a8c..49eaa0991d6a 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts @@ -46,6 +46,10 @@ export function assertClaudeMcpToolPresentationOutput( }); assert.equal(map?.status, "completed"); - assert.isNull(map?.title); - assert.notProperty(map, "toolSource"); + assert.equal(map?.title, "firecrawl map"); + assert.deepEqual(map?.toolSource, { + key: "mcp:firecrawl", + name: "Firecrawl", + kind: "integration", + }); } diff --git a/apps/server/src/provider/CodexToolPresentation.ts b/apps/server/src/provider/CodexToolPresentation.ts index 6bf7acee05b2..3ca112a97de8 100644 --- a/apps/server/src/provider/CodexToolPresentation.ts +++ b/apps/server/src/provider/CodexToolPresentation.ts @@ -6,25 +6,18 @@ import type { } from "@t3tools/contracts"; import type * as EffectCodexSchema from "effect-codex-app-server/schema"; +import { + mcpToolPresentation as integrationToolPresentation, + normalizeMcpHttpUrl as normalizedHttpUrl, + normalizeMcpText as normalizedDisplayName, +} from "./McpToolPresentation.ts"; + type CodexLifecycleItem = EffectCodexSchema.V2ItemCompletedNotification["item"]; function asUnknownRecord(value: unknown): Record | undefined { return value && typeof value === "object" ? (value as Record) : undefined; } -function normalizedHttpUrl(value: unknown): string | undefined { - if (typeof value !== "string" || value.length > 4096) return undefined; - try { - const url = new URL(value); - const href = url.href; - return (url.protocol === "http:" || url.protocol === "https:") && href.length <= 4096 - ? href - : undefined; - } catch { - return undefined; - } -} - function normalizedImageUrl(value: unknown): string | undefined { if (typeof value !== "string" || value.length > 4096) return undefined; try { @@ -45,12 +38,6 @@ function normalizedAppId(value: unknown): string | undefined { : undefined; } -function normalizedDisplayName(value: unknown): string | undefined { - if (typeof value !== "string") return undefined; - const displayName = value.trim().replace(/\s+/gu, " "); - return displayName && displayName.length <= 160 ? displayName : undefined; -} - function normalizedSourceKeyPart(value: string): string { return value.trim().toLowerCase(); } @@ -130,6 +117,7 @@ function themedLogoIcon( } export interface McpToolPresentation { + readonly title?: string; readonly toolSurface?: "browser" | "computer"; readonly toolIcon?: ToolActivityIcon; readonly toolSource?: ToolActivitySource; @@ -227,5 +215,12 @@ export function mcpToolPresentation( }; } - return {}; + return integrationToolPresentation({ + serverName: appContext?.connectorId ?? item.server, + toolName: item.tool, + title: appContext?.actionName, + serverDisplayName: appContext?.appName ?? sourceMetadata?.name, + iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, + iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, + }); } diff --git a/apps/server/src/provider/McpToolPresentation.test.ts b/apps/server/src/provider/McpToolPresentation.test.ts new file mode 100644 index 000000000000..4a9fa44454a9 --- /dev/null +++ b/apps/server/src/provider/McpToolPresentation.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "@effect/vitest"; +import { mcpToolPresentation } from "./McpToolPresentation.ts"; + +describe("mcpToolPresentation", () => { + it("uses supplied names and logos while retaining the server identity", () => { + expect( + mcpToolPresentation({ + serverName: "firecrawl-local", + toolName: "firecrawl_scrape", + title: " Scrape\n page ", + serverDisplayName: "Firecrawl", + iconUrl: "https://example.com/icon.png", + iconUrlDark: "https://example.com/dark.png", + }), + ).toEqual({ + title: "Scrape page", + toolSource: { + key: "mcp:firecrawl-local", + name: "Firecrawl", + kind: "integration", + icon: { + _tag: "themed-logo", + logoUrl: "https://example.com/icon.png", + logoUrlDark: "https://example.com/dark.png", + }, + }, + }); + }); + + it("uses readable fallback names without a guessed logo", () => { + expect(mcpToolPresentation({ toolName: "mcp__my_server__get_weather" })).toEqual({ + title: "get weather", + toolSource: { key: "mcp:my_server", name: "my server", kind: "integration" }, + }); + expect(mcpToolPresentation({ toolName: "Read" })).toEqual({}); + expect(mcpToolPresentation({ serverName: "t3-code", toolName: "delegate_task" })).toEqual({}); + }); + + it.each([ + "javascript:alert(1)", + "file:///tmp/logo.png", + "not a URL", + "https://example.com/" + "x".repeat(4096), + ])("ignores an invalid logo %s", (iconUrl) => { + expect( + mcpToolPresentation({ + serverName: "weather", + toolName: "get_weather", + iconUrl, + title: "x".repeat(161), + }), + ).toEqual({ + title: "get weather", + toolSource: { key: "mcp:weather", name: "weather", kind: "integration" }, + }); + }); + + it("rejects malformed and overlong names", () => { + expect(mcpToolPresentation({ serverName: {}, toolName: 1, title: [] })).toEqual({}); + expect(mcpToolPresentation({ serverName: "x".repeat(161), toolName: "get_weather" })).toEqual( + {}, + ); + expect(mcpToolPresentation({ title: "Tool title" })).toEqual({ title: "Tool title" }); + }); +}); diff --git a/apps/server/src/provider/McpToolPresentation.ts b/apps/server/src/provider/McpToolPresentation.ts new file mode 100644 index 000000000000..6f5f37a1b596 --- /dev/null +++ b/apps/server/src/provider/McpToolPresentation.ts @@ -0,0 +1,62 @@ +import type { ToolActivitySource } from "@t3tools/contracts"; +import { resolveT3McpToolDefinition } from "@t3tools/shared/t3McpToolPresentation"; + +export function normalizeMcpText(value: unknown, maxLength = 160): string | undefined { + if (typeof value !== "string") return undefined; + const text = value.trim().replace(/\s+/gu, " "); + return text.length > 0 && text.length <= maxLength ? text : undefined; +} + +export function normalizeMcpHttpUrl(value: unknown): string | undefined { + if (typeof value !== "string" || value.length > 4096) return undefined; + try { + const url = new URL(value); + return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096 + ? url.href + : undefined; + } catch { + return undefined; + } +} + +export function mcpToolPresentation(input: { + readonly toolName?: unknown; + readonly serverName?: unknown; + readonly title?: unknown; + readonly serverDisplayName?: unknown; + readonly iconUrl?: unknown; + readonly iconUrlDark?: unknown; +}): { readonly title?: string; readonly toolSource?: ToolActivitySource } { + const qualified = + typeof input.toolName === "string" ? /^mcp__(.+?)__(.+)$/i.exec(input.toolName) : null; + const server = normalizeMcpText(input.serverName ?? qualified?.[1] ?? input.serverDisplayName); + const tool = normalizeMcpText(qualified?.[2] ?? input.toolName); + if (server && tool && resolveT3McpToolDefinition(`${server}.${tool}`)) return {}; + const title = + normalizeMcpText(input.title) ?? + (server && tool ? normalizeMcpText(tool.replace(/[_-]+/gu, " ")) : undefined); + if (!server) return title ? { title } : {}; + const name = + normalizeMcpText(input.serverDisplayName) ?? + normalizeMcpText(server.replace(/[_-]+/gu, " ")) ?? + server; + const logoUrl = normalizeMcpHttpUrl(input.iconUrl); + const logoUrlDark = normalizeMcpHttpUrl(input.iconUrlDark); + return { + ...(title ? { title } : {}), + toolSource: { + key: `mcp:${server.toLowerCase()}`, + name, + kind: "integration", + ...(logoUrl + ? { + icon: { + _tag: "themed-logo", + logoUrl, + ...(logoUrlDark ? { logoUrlDark } : {}), + } as const, + } + : {}), + }, + }; +} diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts index e69c851b258e..e16e83d5829c 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts @@ -1539,9 +1539,29 @@ describe("extractMcpToolCallIdentity", () => { }); }); - it("does not brand tools whose meta asserts a foreign server", () => { - // The foreign assertion vetoes every loose source, including a title - // that would otherwise match a T3 convention. + it.each([ + { + _meta: { + goose: { toolCall: { toolName: "weather__get_weather", extensionName: "weather" } }, + }, + }, + { _meta: { claudeCode: { toolName: "mcp__weather__get_weather" } } }, + { _meta: { toolName: "mcp::weather::get_weather", serverId: "weather" } }, + ])("recovers external MCP identity from provider metadata", (metadata) => { + const toolCall = toolCallFromUpdate({ + sessionUpdate: "tool_call", + toolCallId: "weather-call", + title: "Checking the forecast", + status: "in_progress", + ...metadata, + }); + expect(extractMcpToolCallIdentity(toolCall)).toEqual({ + server: "weather", + tool: "get_weather", + }); + }); + + it("uses the asserted server instead of a misleading T3 title", () => { const toolCall = toolCallFromUpdate({ sessionUpdate: "tool_call", toolCallId: "foreign-1", @@ -1551,7 +1571,10 @@ describe("extractMcpToolCallIdentity", () => { _meta: { toolName: "delegate_task", serverId: "other-orchestrator" }, }); - expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined(); + expect(extractMcpToolCallIdentity(toolCall)).toEqual({ + server: "other-orchestrator", + tool: "delegate_task", + }); }); it("does not brand path-like or unknown-tool titles", () => { diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.ts b/apps/server/src/provider/acp/AcpRuntimeModel.ts index 8cb059acb2ec..eec9b5b6022c 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.ts @@ -1134,9 +1134,7 @@ export function extractMcpToolCallIdentity( // (goose, qwen, claude-acp) or only through the namespaced function name // in the title. The verbatim wire title survives merges even when a later // titleless or LLM-enriched update replaces the presentation title, so - // match those rather than the summarized state title. Name-derived matches - // are gated on the known T3 tool inventory so path-like titles (for - // example "t3-code/README.md") never brand. + // match those rather than the summarized state title. const claudeCode = isRecord(meta?.claudeCode) ? meta.claudeCode : undefined; const gooseToolCall = isRecord(meta?.goose) ? isRecord(meta.goose.toolCall) @@ -1160,15 +1158,22 @@ export function extractMcpToolCallIdentity( } } } - // A present-but-foreign origin assertion marks the whole call as another - // server's MCP call, so no loose name matching (meta or title) may brand it. const gooseExtension = typeof gooseToolCall?.extensionName === "string" ? gooseToolCall.extensionName.trim() : ""; const assertsForeignOrigin = (metaServerId.length > 0 && !/^t3[-_ ]?code$/i.test(metaServerId)) || (gooseExtension.length > 0 && !/^t3[-_ ]?code$/i.test(gooseExtension)); if (assertsForeignOrigin) { - return undefined; + const server = metaServerId || gooseExtension; + const name = + metaToolName || + (typeof gooseToolCall?.toolName === "string" ? gooseToolCall.toolName.trim() : ""); + if (!name) return undefined; + const prefix = [`mcp__${server}__`, `mcp::${server}::`, `${server}__`].find((prefix) => + name.startsWith(prefix), + ); + const tool = prefix === undefined ? name : name.slice(prefix.length); + return tool ? { server, tool } : undefined; } const candidates = [ meta?.toolName, @@ -1178,6 +1183,9 @@ export function extractMcpToolCallIdentity( ].filter((value): value is string => typeof value === "string"); for (const candidate of candidates) { const trimmed = candidate.trim(); + const qualified = /^mcp__(.+?)__(.+)$/i.exec(trimmed); + if (qualified?.[1] && qualified[2] && !/^t3[-_ ]?code$/i.test(qualified[1])) + return { server: qualified[1], tool: qualified[2] }; const match = T3_MCP_TITLE_CALL.exec(trimmed) ?? T3_MCP_TITLE_SUFFIX_CALL.exec(trimmed) ?? From 8ea5b743c0d4d64487e94126bebd486e72e24798 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:13:00 +0200 Subject: [PATCH 5/5] fix(server): preserve MCP logos on tool activity --- .../Adapters/AcpAdapterV2.test.ts | 46 ++++++++++++++++--- .../orchestration-v2/Adapters/AcpAdapterV2.ts | 9 ++++ .../Adapters/ClaudeAdapterV2.test.ts | 4 ++ .../Adapters/ClaudeAdapterV2.ts | 3 ++ .../Adapters/CodexAdapterV2.test.ts | 15 +++++- .../claude_mcp_tool_presentation/output.ts | 1 + .../src/provider/CodexToolPresentation.ts | 3 +- .../src/provider/McpToolPresentation.test.ts | 38 +++++++++++++++ .../src/provider/McpToolPresentation.ts | 34 ++++++++------ 9 files changed, 131 insertions(+), 22 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 27c7380d7f26..13807f3336c1 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1081,11 +1081,33 @@ describe("AcpAdapterV2", () => { title: "Check weather", status: "completed", rawInput: { server: "weather", tool: "get_weather", city: "Berlin" }, + rawOutput: { + result: { + _meta: { + source: { name: "Weather", logoUrl: "https://example.com/weather.png" }, + }, + content: [{ type: "text", text: "Sunny" }], + }, + }, _meta: { is_mcp_tool_call: true, "cognition.ai/subagent_context": { parentAgentId: "child-a" }, }, }, + { + sessionUpdate: "tool_call", + toolCallId: "parent-weather", + title: "Check weather", + status: "completed", + rawInput: { server: "weather", tool: "get_weather", city: "Berlin" }, + rawOutput: { + _meta: { + source: { name: "Weather", logoUrl: "https://example.com/weather.png" }, + }, + content: [{ type: "text", text: "Sunny" }], + }, + _meta: { is_mcp_tool_call: true }, + }, { sessionUpdate: "agent_message_chunk", content: { type: "text", text: " → decider → event)." }, @@ -1209,12 +1231,22 @@ describe("AcpAdapterV2", () => { item.type === "dynamic_tool" && item.toolName === "weather.get_weather", ); - assert.equal(childMcp?.title, "get weather"); - assert.deepEqual(childMcp?.toolSource, { - key: "mcp:weather", - name: "weather", - kind: "integration", - }); + const parentMcp = items.find( + (item) => + item.threadId === threadId && + item.type === "dynamic_tool" && + item.toolName === "weather.get_weather", + ); + for (const item of [parentMcp, childMcp]) { + assert.equal(item?.title, "get weather"); + assert.deepEqual(item?.toolSource, { + key: "mcp:weather", + name: "Weather", + kind: "integration", + icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" }, + }); + assert.deepEqual(item?.toolIcon, item?.toolSource?.icon); + } assert.isTrue( items.some( (item) => @@ -1239,7 +1271,7 @@ describe("AcpAdapterV2", () => { const parentTools = items.filter( (item) => item.threadId === threadId && item.type === "dynamic_tool", ); - assert.equal(parentTools.length, 1); + assert.equal(parentTools.length, 2); assert.equal(parentTools[0]?.title, "Parent tool finished"); }).pipe(Effect.provide(testLayer), Effect.scoped), ); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts index 90dd098ad503..621073745d5b 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts @@ -3263,6 +3263,9 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV ...mcpToolPresentation({ serverName: mcpIdentity.server, toolName: mcpIdentity.tool, + source: unknownRecord( + (unknownRecord(rawOutputRecord?.result) ?? rawOutputRecord)?._meta, + )?.source, }), type: "dynamic_tool", toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`, @@ -4318,6 +4321,12 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV : mcpToolPresentation({ serverName: mcpIdentity.server, toolName: mcpIdentity.tool, + source: unknownRecord( + ( + unknownRecord(unknownRecord(merged.data.rawOutput)?.result) ?? + unknownRecord(merged.data.rawOutput) + )?._meta, + )?.source, })), toolName: mcpIdentity === undefined diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index c491bcc641a4..e4923d5454f8 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -2221,6 +2221,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { assert.isNull(items[0]?.title); for (const item of items.slice(1)) { assert.equal(item.title, title ?? "get weather"); + assert.deepEqual(item.toolIcon, { + _tag: "themed-logo", + logoUrl: "https://example.com/weather.png", + }); assert.deepEqual(item.toolSource, { key: "mcp:weather", name: "Weather", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 968f8d4e9958..facbad4f96da 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3697,6 +3697,9 @@ export function makeClaudeAdapterV2( : { ...itemBase, type: "dynamic_tool", + ...(input.presentation?.toolIcon === undefined + ? {} + : { toolIcon: input.presentation.toolIcon }), ...(input.presentation?.toolSource === undefined ? {} : { toolSource: input.presentation.toolSource }), diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 7cc64cb2623c..c9cb10754fad 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -843,10 +843,23 @@ describe("CodexAdapterV2 dynamic tool projection", () => { tool: "connector_get_weather", status: "completed", arguments: { title: "Argument, not display metadata" }, - appContext: { connectorId: "weather-app", appName: "Weather", actionName: "Check weather" }, + appContext: { + connectorId: "weather-app", + appName: "Weather", + actionName: "Check weather", + }, + result: { + content: [], + _meta: { source: { logoUrl: "https://example.com/weather.png" } }, + }, }); assert.equal(projection.title, "Check weather"); assert.equal(projection.toolSource?.name, "Weather"); + assert.deepEqual(projection.toolIcon, { + _tag: "themed-logo", + logoUrl: "https://example.com/weather.png", + }); + assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); }); it("preserves native browser and app icons alongside MCP tool output", () => { diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts index 49eaa0991d6a..b51ca4f52f4a 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts @@ -35,6 +35,7 @@ export function assertClaudeMcpToolPresentationOutput( assert.equal(scrape?.status, "completed"); assert.equal(scrape?.title, "Firecrawl scrape"); + assert.deepEqual(scrape?.toolIcon, scrape?.toolSource?.icon); assert.deepEqual(scrape?.toolSource, { key: "mcp:firecrawl", name: "Firecrawl", diff --git a/apps/server/src/provider/CodexToolPresentation.ts b/apps/server/src/provider/CodexToolPresentation.ts index 3ca112a97de8..a01390582bda 100644 --- a/apps/server/src/provider/CodexToolPresentation.ts +++ b/apps/server/src/provider/CodexToolPresentation.ts @@ -219,7 +219,8 @@ export function mcpToolPresentation( serverName: appContext?.connectorId ?? item.server, toolName: item.tool, title: appContext?.actionName, - serverDisplayName: appContext?.appName ?? sourceMetadata?.name, + serverDisplayName: appContext?.appName, + source: sourceMetadata, iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, }); diff --git a/apps/server/src/provider/McpToolPresentation.test.ts b/apps/server/src/provider/McpToolPresentation.test.ts index 4a9fa44454a9..5218d05ebeb8 100644 --- a/apps/server/src/provider/McpToolPresentation.test.ts +++ b/apps/server/src/provider/McpToolPresentation.test.ts @@ -14,6 +14,11 @@ describe("mcpToolPresentation", () => { }), ).toEqual({ title: "Scrape page", + toolIcon: { + _tag: "themed-logo", + logoUrl: "https://example.com/icon.png", + logoUrlDark: "https://example.com/dark.png", + }, toolSource: { key: "mcp:firecrawl-local", name: "Firecrawl", @@ -27,6 +32,25 @@ describe("mcpToolPresentation", () => { }); }); + it("uses MCP result source metadata for the tool and its integration", () => { + const presentation = mcpToolPresentation({ + serverName: "firecrawl-local", + toolName: "firecrawl_scrape", + source: { + name: "Firecrawl", + logoUrl: "https://example.com/firecrawl.png", + logoUrlDark: "https://example.com/firecrawl-dark.png", + }, + }); + expect(presentation.toolSource?.name).toBe("Firecrawl"); + expect(presentation.toolIcon).toEqual({ + _tag: "themed-logo", + logoUrl: "https://example.com/firecrawl.png", + logoUrlDark: "https://example.com/firecrawl-dark.png", + }); + expect(presentation.toolSource?.icon).toEqual(presentation.toolIcon); + }); + it("uses readable fallback names without a guessed logo", () => { expect(mcpToolPresentation({ toolName: "mcp__my_server__get_weather" })).toEqual({ title: "get weather", @@ -62,4 +86,18 @@ describe("mcpToolPresentation", () => { ); expect(mcpToolPresentation({ title: "Tool title" })).toEqual({ title: "Tool title" }); }); + + it.each([ + null, + "logo", + { logoUrl: "file:///tmp/logo.png" }, + { logoUrlDark: "https://example.com/dark.png" }, + ])("ignores source metadata without a valid logo: %j", (source) => { + expect(mcpToolPresentation({ serverName: "weather", toolName: "get_weather", source })).toEqual( + { + title: "get weather", + toolSource: { key: "mcp:weather", name: "weather", kind: "integration" }, + }, + ); + }); }); diff --git a/apps/server/src/provider/McpToolPresentation.ts b/apps/server/src/provider/McpToolPresentation.ts index 6f5f37a1b596..c8f4dbc0eb11 100644 --- a/apps/server/src/provider/McpToolPresentation.ts +++ b/apps/server/src/provider/McpToolPresentation.ts @@ -1,4 +1,4 @@ -import type { ToolActivitySource } from "@t3tools/contracts"; +import type { ToolActivityIcon, ToolActivitySource } from "@t3tools/contracts"; import { resolveT3McpToolDefinition } from "@t3tools/shared/t3McpToolPresentation"; export function normalizeMcpText(value: unknown, maxLength = 160): string | undefined { @@ -26,7 +26,14 @@ export function mcpToolPresentation(input: { readonly serverDisplayName?: unknown; readonly iconUrl?: unknown; readonly iconUrlDark?: unknown; -}): { readonly title?: string; readonly toolSource?: ToolActivitySource } { + readonly source?: unknown; +}): { + readonly title?: string; + readonly toolIcon?: ToolActivityIcon; + readonly toolSource?: ToolActivitySource; +} { + const source = + typeof input.source === "object" && input.source !== null ? input.source : undefined; const qualified = typeof input.toolName === "string" ? /^mcp__(.+?)__(.+)$/i.exec(input.toolName) : null; const server = normalizeMcpText(input.serverName ?? qualified?.[1] ?? input.serverDisplayName); @@ -38,25 +45,26 @@ export function mcpToolPresentation(input: { if (!server) return title ? { title } : {}; const name = normalizeMcpText(input.serverDisplayName) ?? + normalizeMcpText(source && Reflect.get(source, "name")) ?? normalizeMcpText(server.replace(/[_-]+/gu, " ")) ?? server; - const logoUrl = normalizeMcpHttpUrl(input.iconUrl); - const logoUrlDark = normalizeMcpHttpUrl(input.iconUrlDark); + const logoUrl = + normalizeMcpHttpUrl(input.iconUrl) ?? + normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrl")); + const logoUrlDark = + normalizeMcpHttpUrl(input.iconUrlDark) ?? + normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrlDark")); + const icon = logoUrl + ? { _tag: "themed-logo" as const, logoUrl, ...(logoUrlDark ? { logoUrlDark } : {}) } + : undefined; return { ...(title ? { title } : {}), + ...(icon ? { toolIcon: icon } : {}), toolSource: { key: `mcp:${server.toLowerCase()}`, name, kind: "integration", - ...(logoUrl - ? { - icon: { - _tag: "themed-logo", - logoUrl, - ...(logoUrlDark ? { logoUrlDark } : {}), - } as const, - } - : {}), + ...(icon ? { icon } : {}), }, }; }