From cf4173a100eb675a0b0c52dc3f2c4341d9cdc40c Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 14 May 2026 05:51:13 +0200 Subject: [PATCH] feat(exa): replaced raw JSON responses with human-readable formatting - Added formatGenericResponse to handle objects, arrays, MCP content blocks, and primitives. - Applied formatted output to MCPWrappedTool, createExaTool, and propose-changelog tool. - Replaced raw response in details field with structured response object. --- .../commit/agentic/tools/propose-changelog.ts | 9 ++- packages/coding-agent/src/exa/factory.ts | 6 +- packages/coding-agent/src/exa/mcp-client.ts | 77 ++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts b/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts index 9b5cfaf03..6ccf0e929 100644 --- a/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts +++ b/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts @@ -130,8 +130,15 @@ export function createProposeChangelogTool( state.changelogProposal = { entries: normalized }; } + let text = response.valid ? "Changelog entries accepted." : "Changelog validation failed."; + if (response.errors.length > 0) { + text += `\n\nErrors:\n${response.errors.map(e => `- ${e}`).join("\n")}`; + } + if (response.warnings.length > 0) { + text += `\n\nWarnings:\n${response.warnings.map(w => `- ${w}`).join("\n")}`; + } return { - content: [{ type: "text", text: JSON.stringify(response, null, 2) }], + content: [{ type: "text", text }], details: response, }; }, diff --git a/packages/coding-agent/src/exa/factory.ts b/packages/coding-agent/src/exa/factory.ts index 30c2ef9ac..8888a838c 100644 --- a/packages/coding-agent/src/exa/factory.ts +++ b/packages/coding-agent/src/exa/factory.ts @@ -3,7 +3,7 @@ */ import type { TObject, TProperties } from "@sinclair/typebox"; import type { CustomTool } from "../extensibility/custom-tools/types"; -import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client"; +import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client"; import type { ExaRenderDetails } from "./types"; /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */ @@ -44,8 +44,8 @@ export function createExaTool( } return { - content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }], - details: { raw: response, toolName: name }, + content: [{ type: "text" as const, text: formatGenericResponse(response) }], + details: { response, toolName: name }, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/packages/coding-agent/src/exa/mcp-client.ts b/packages/coding-agent/src/exa/mcp-client.ts index 98f4a6ebb..1470169aa 100644 --- a/packages/coding-agent/src/exa/mcp-client.ts +++ b/packages/coding-agent/src/exa/mcp-client.ts @@ -174,6 +174,79 @@ export function formatSearchResults(data: ExaSearchResponse): string { return output.trim(); } +/** + * Format a non-search MCP response as human-readable text. + * Handles objects, arrays, primitives, and common MCP response shapes. + */ +export function formatGenericResponse(data: unknown): string { + if (data === null || data === undefined) return "No result."; + if (typeof data === "string") return data; + if (typeof data === "number" || typeof data === "boolean") return String(data); + + if (Array.isArray(data)) { + if (data.length === 0) return "(empty)"; + const parts: string[] = []; + for (let i = 0; i < data.length; i++) { + const item = data[i]; + if (typeof item === "object" && item !== null) { + const record = item as Record; + const title = (record.title ?? record.name ?? record.id ?? `Item ${i + 1}`) as string; + parts.push(`\n### ${title}`); + for (const [k, v] of Object.entries(record)) { + if (["title", "name", "id"].includes(k)) continue; + parts.push(`- **${k}:** ${formatValue(v)}`); + } + } else { + parts.push(`- ${formatValue(item)}`); + } + } + return parts.join("\n"); + } + + if (typeof data === "object") { + const record = data as Record; + if (record.content && Array.isArray(record.content)) { + // MCP-style content array — extract text blocks + const texts = record.content + .filter( + (c: unknown): c is { type: string; text?: string } => + typeof c === "object" && c !== null && (c as Record)?.type === "text", + ) + .map(c => c.text ?? "") + .filter(Boolean); + if (texts.length > 0) return texts.join("\n"); + } + + const lines: string[] = []; + for (const [k, v] of Object.entries(record)) { + if (k === "content") continue; // handled above + if (v === null || v === undefined) continue; + if (typeof v === "object") { + const formatted = formatGenericResponse(v); + if (formatted) lines.push(`- **${k}:**\n${indent(formatted, 2)}`); + } else { + lines.push(`- **${k}:** ${formatValue(v)}`); + } + } + return lines.join("\n") || "(empty)"; + } + + return String(data); +} + +function formatValue(v: unknown): string { + if (v === null || v === undefined) return "—"; + if (typeof v === "object") return JSON.stringify(v); + return String(v); +} + +function indent(text: string, spaces: number): string { + const pad = " ".repeat(spaces); + return text + .split("\n") + .map(line => pad + line) + .join("\n"); +} /** Check if result is a search response */ export function isSearchResponse(data: unknown): data is ExaSearchResponse { @@ -260,8 +333,8 @@ export class MCPWrappedTool implements CustomTool { } return { - content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }], - details: { raw: response, toolName: this.config.name }, + content: [{ type: "text" as const, text: formatGenericResponse(response) }], + details: { response, toolName: this.config.name }, }; } catch (error) { const message = error instanceof Error ? error.message : String(error);