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 <Pluviobyte@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
rain
2026-05-29 00:29:29 +08:00
committed by GitHub
parent cd0b692b61
commit 2df8021cda
2 changed files with 71 additions and 27 deletions

View File

@@ -27,30 +27,33 @@ function toAgentToolResult(params: {
const content = Array.isArray(params.result.content)
? (params.result.content as AgentToolResult<unknown>["content"])
: [];
const normalizedContent: AgentToolResult<unknown>["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<unknown>["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<unknown>["content"]);
: ([
{
type: "text",
text: JSON.stringify(
{
status: params.result.isError === true ? "error" : "ok",
server: params.serverName,
tool: params.toolName,
},
null,
2,
),
},
] as AgentToolResult<unknown>["content"]);
const details: Record<string, unknown> = {
mcpServer: params.serverName,
mcpTool: params.toolName,

View File

@@ -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(),