From 2df8021cdab84b6709d904643dca5163211f3045 Mon Sep 17 00:00:00 2001 From: rain <1050807841@qq.com> Date: Fri, 29 May 2026 00:29:29 +0800 Subject: [PATCH] fix(agents): surface MCP structured content in tool results Surface inbound bundle-MCP structuredContent as the model-visible result when present so agents can read Codex MCP threadId values and continue with codex-reply. Preserve non-structured content behavior, preserve the empty-result fallback, and keep details.structuredContent for internal consumers. Also remove an unused secrets path helper that was breaking the latest prod-type gate on main. Fixes #87511. Verification: - node scripts/run-vitest.mjs src/agents/agent-bundle-mcp-tools.materialize.test.ts - pnpm exec oxfmt --check src/secrets/path-utils.ts src/agents/agent-bundle-mcp-materialize.ts src/agents/agent-bundle-mcp-tools.materialize.test.ts - pnpm tsgo:prod - local check-guards shard commands - live Codex MCP smoke with codex__codex and codex__codex-reply same-thread continuation - autoreview clean - CI run 26587222874 green Co-authored-by: Pluviobyte Co-authored-by: Cursor --- src/agents/agent-bundle-mcp-materialize.ts | 49 ++++++++++--------- ...agent-bundle-mcp-tools.materialize.test.ts | 49 +++++++++++++++++-- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/agents/agent-bundle-mcp-materialize.ts b/src/agents/agent-bundle-mcp-materialize.ts index cc5224a9853..c549af48cd2 100644 --- a/src/agents/agent-bundle-mcp-materialize.ts +++ b/src/agents/agent-bundle-mcp-materialize.ts @@ -27,30 +27,33 @@ function toAgentToolResult(params: { const content = Array.isArray(params.result.content) ? (params.result.content as AgentToolResult["content"]) : []; - const normalizedContent: AgentToolResult["content"] = - content.length > 0 + const structuredContentBlock = + params.result.structuredContent !== undefined + ? ({ + type: "text", + text: `structuredContent:\n${JSON.stringify(params.result.structuredContent, null, 2)}`, + } as const) + : null; + // Structured MCP results are the canonical model payload here; replacing + // mirrored content avoids duplicating large tool output in the prompt. + const normalizedContent: AgentToolResult["content"] = structuredContentBlock + ? [structuredContentBlock] + : content.length > 0 ? content - : params.result.structuredContent !== undefined - ? [ - { - type: "text", - text: JSON.stringify(params.result.structuredContent, null, 2), - }, - ] - : ([ - { - type: "text", - text: JSON.stringify( - { - status: params.result.isError === true ? "error" : "ok", - server: params.serverName, - tool: params.toolName, - }, - null, - 2, - ), - }, - ] as AgentToolResult["content"]); + : ([ + { + type: "text", + text: JSON.stringify( + { + status: params.result.isError === true ? "error" : "ok", + server: params.serverName, + tool: params.toolName, + }, + null, + 2, + ), + }, + ] as AgentToolResult["content"]); const details: Record = { mcpServer: params.serverName, mcpTool: params.toolName, diff --git a/src/agents/agent-bundle-mcp-tools.materialize.test.ts b/src/agents/agent-bundle-mcp-tools.materialize.test.ts index aed8db5100b..cdfa1a88dcc 100644 --- a/src/agents/agent-bundle-mcp-tools.materialize.test.ts +++ b/src/agents/agent-bundle-mcp-tools.materialize.test.ts @@ -1,3 +1,4 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { validateToolArguments } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { getPluginToolMeta } from "../plugins/tools.js"; @@ -18,6 +19,7 @@ function makeToolRuntime( params: { tools?: McpCatalogTool[]; serverName?: string; + result?: CallToolResult; resultText?: string; } = {}, ): SessionMcpRuntime { @@ -63,10 +65,11 @@ function makeToolRuntime( }, tools, }), - callTool: async () => ({ - content: [{ type: "text", text: params.resultText ?? "FROM-BUNDLE" }], - isError: false, - }), + callTool: async () => + params.result ?? { + content: [{ type: "text", text: params.resultText ?? "FROM-BUNDLE" }], + isError: false, + }, dispose: async () => {}, }; } @@ -87,6 +90,44 @@ describe("createBundleMcpToolRuntime", () => { }); }); + it("keeps structuredContent visible when MCP tools also return text content", async () => { + const runtime = await materializeBundleMcpToolsForRun({ + runtime: makeToolRuntime({ + result: { + content: [{ type: "text", text: "pong" }], + structuredContent: { + threadId: "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7", + content: "pong", + }, + isError: false, + }, + }), + }); + + const result = await runtime.tools[0].execute("call-bundle-probe", {}, undefined, undefined); + + expectTextContentBlock( + result.content[0], + `structuredContent:\n${JSON.stringify( + { + threadId: "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7", + content: "pong", + }, + null, + 2, + )}`, + ); + expect(result.content).toHaveLength(1); + expect(result.details).toEqual({ + mcpServer: "bundleProbe", + mcpTool: "bundle_probe", + structuredContent: { + threadId: "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7", + content: "pong", + }, + }); + }); + it("disambiguates bundle MCP tools that collide with existing tool names", async () => { const runtime = await materializeBundleMcpToolsForRun({ runtime: makeToolRuntime(),