From 037e92b421f6793e369dcae594052fb00e1a9386 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 24 May 2026 09:33:12 +0100 Subject: [PATCH] fix(bridge): /mcp/execute handles raw-object tool results (not just AgentToolResult) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw plugins return tool results in one of two shapes: (a) AgentToolResult — { content: [{type:'text', text:'...'}] } used when the plugin wraps via asContent() helper. Every Dialectic.OpenclawPlugin tool follows this pattern. (b) raw JSON-able object — { ok:true, ...domain fields } used when the plugin returns data directly. Every Fabric.OpenclawPlugin tool follows this pattern (fabric-channel-list, fabric-guild-list, fabric-send-message, fabric-channel-set-purpose, etc). The bridge's /mcp/execute handler only handled shape (a). When a contractor agent (developer / contractor-test) called any fabric tool through Claude Code, the bridge ran the tool successfully but fell back to the literal string '(no result)' because toolResult.content was undefined. Claude Code then dutifully rendered '(no result)' as the tool result. Reproduced on prod: openclaw agent --agent developer -m 'Call fabric-channel-list ...' → claude code session called mcp__openclaw__fabric-channel-list → bridge logged: mcp/execute tool=fabric-channel-list ... → bridge replied: { result: '(no result)' } → claude code rendered: '' Fix: normalize the result in the bridge. If toolResult is null → empty string; if it has a .content array → join the text segments (shape a); if it's a string → use directly; else → JSON.stringify the whole thing (shape b). Falls back to '(no result)' only when all of those produce empty string. Verified on prod after fix: agent receives real {"ok":true,"count":1,"channels":[...]} JSON payload (one real prod-push-test channel) in the response. --- plugin/web/server.ts | 47 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/plugin/web/server.ts b/plugin/web/server.ts index 45f4776..7af7e53 100644 --- a/plugin/web/server.ts +++ b/plugin/web/server.ts @@ -596,14 +596,47 @@ export function createBridgeServer(config: BridgeServerConfig): http.Server { return; } - const execFn = (toolInstance as { execute: (id: string, args: unknown) => Promise<{ content?: Array<{ type: string; text?: string }> }> }).execute; - const toolResult = await execFn(randomUUID(), toolArgs); + const execFn = (toolInstance as { execute: (id: string, args: unknown) => Promise }).execute; + const toolResultRaw = await execFn(randomUUID(), toolArgs); - // Extract text content from AgentToolResult - const text = (toolResult.content ?? []) - .filter((c) => c.type === "text" && c.text) - .map((c) => c.text as string) - .join("\n"); + // Normalize the result shape. OpenClaw plugins return one of: + // (a) AgentToolResult — { content: [{type:'text', text:'...'}, ...] } + // used by tools that wrap via asContent() (e.g. all + // Dialectic.OpenclawPlugin tools). + // (b) raw JSON-able object — { ok:true, ...domain fields } + // used by tools that return data directly (e.g. every + // Fabric.OpenclawPlugin tool: fabric-channel-list, + // fabric-guild-list, fabric-send-message, etc). + // (c) null / undefined — fire-and-forget (rare). + // + // Pre-2026-05-24 the bridge ONLY handled shape (a); shape (b) + // silently returned "(no result)" to Claude Code (bug: + // fabric-channel-list and friends were unusable from contractor + // agents even though the tool actually ran successfully). + let text = ""; + if (toolResultRaw == null) { + text = ""; + } else if ( + typeof toolResultRaw === "object" && + Array.isArray((toolResultRaw as { content?: unknown }).content) + ) { + // Shape (a) + const content = (toolResultRaw as { content: Array<{ type: string; text?: string }> }).content; + text = content + .filter((c) => c.type === "text" && c.text) + .map((c) => c.text as string) + .join("\n"); + } else if (typeof toolResultRaw === "string") { + text = toolResultRaw; + } else { + // Shape (b) — serialize the whole object as JSON. Claude Code + // is happy to parse JSON tool results. + try { + text = JSON.stringify(toolResultRaw); + } catch { + text = String(toolResultRaw); + } + } sendJson(res, 200, { result: text || "(no result)" }); } catch (err) {