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); }