From 1c4cf773e5a21f603bea20a0a67ae34f1daf48c6 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 23 May 2026 08:28:27 +0100 Subject: [PATCH] fix(hf-plugin): wrap tool returns in MCP {content:[...]} shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw's Codex tool dispatcher (thread-lifecycle:255) expects every tool execute() to return { content: [...] } and calls result.content.reduce() to compute total text length. All 9 harborforge_* tools returned bare objects ({ running, processing, currentSlot, ... }) which has no .content field — so .reduce of undefined threw, and the agent saw the cryptic 'Cannot read properties of undefined (reading reduce)' on every call. This silently blocked every calendar slot transition on prod for hours: agents could call harborforge_calendar_complete but it always errored, so slots never moved out of not_started. Fix is at the registerTool boundary: api.registerTool is wrapped once to coerce every tool's execute return through ensureMcpContentShape. Tools that already return the correct shape are unchanged. No per-tool edits needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/index.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/plugin/index.ts b/plugin/index.ts index 2058c21..3fe19f4 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -73,6 +73,30 @@ interface PluginAPI { getAgentStatus?: () => Promise<{ status: string } | null>; } +/** + * Coerce a tool execute() return value into the MCP `{ content: [...] }` + * shape that the openclaw Codex tool dispatcher requires. + * + * Background: openclaw's `convertToolContents()` does `result.content.reduce(...)` + * to compute total text length before flattening. Every HF tool here returned a + * bare object (`{ running, processing, currentSlot, ... }`) which has no + * `.content` field, so `undefined.reduce` threw and every call to + * `harborforge_*` from a Codex-harness agent surfaced as the cryptic + * `Cannot read properties of undefined (reading 'reduce')`. The fix is to + * wrap every tool's execute return; doing it at the `registerTool` boundary + * keeps each tool body unchanged. + */ +function ensureMcpContentShape(result: unknown): { content: Array<{ type: 'text'; text: string }> } { + if ( + result && typeof result === 'object' && + Array.isArray((result as { content?: unknown }).content) + ) { + return result as { content: Array<{ type: 'text'; text: string }> }; + } + const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2); + return { content: [{ type: 'text', text }] }; +} + function register(api: PluginAPI): void { const logger = api.logger || { info: (...args: any[]) => console.log('[HarborForge]', ...args), @@ -81,6 +105,22 @@ function register(api: PluginAPI): void { warn: (...args: any[]) => console.warn('[HarborForge]', ...args), }; + // Wrap api.registerTool so every tool's execute() return is coerced into + // the MCP `{ content: [...] }` shape openclaw expects. See + // `ensureMcpContentShape` above. + const _origRegisterTool = api.registerTool.bind(api); + api.registerTool = (factory: (ctx: any) => any) => { + _origRegisterTool((ctx: any) => { + const def = factory(ctx); + if (!def || typeof def.execute !== 'function') return def; + const origExecute = def.execute; + return { + ...def, + execute: async (...args: any[]) => ensureMcpContentShape(await origExecute(...args)), + }; + }); + }; + function resolveConfig() { return getPluginConfig(api); }