{"record":{"id":"9a4975a465cacd1a","repo":"CherryHQ/cherry-studio","slug":"read-file-returned-an-unexpected-output-type","errorCode":null,"errorMessage":"read_file returned an unexpected output type","messagePattern":"read_file returned an unexpected output type","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/AssistantFileToolsServer.ts","lineNumber":59,"sourceCode":"  const inputSchema = z.toJSONSchema(handler.inputSchema) as Record<string, unknown>\n  delete inputSchema.$schema\n  return { name, description: handler.description, inputSchema: inputSchema as Tool['inputSchema'] }\n}\n\nexport class AssistantFileToolsServer {\n  public readonly mcpServer: McpServer\n  private readonly handlers: Record<string, AssistantFileToolHandler>\n\n  constructor(context: AssistantFileToolContext) {\n    this.handlers = {\n      [READ_FILE_TOOL_NAME]: {\n        description: READ_FILE_DESCRIPTION,\n        inputSchema: readFileInputSchema,\n        run: async (args, signal) => {\n          const input = readFileInputSchema.parse(args)\n          const result = await readFile(input, { attachments: listAgentSessionAttachments(context.sessionId) }, signal)\n          const output = readFileModelOutput(result)\n          if (output.type !== 'text') throw new Error('read_file returned an unexpected output type')\n          return output.value\n        }\n      },\n      [SAVE_ATTACHMENT_TOOL_NAME]: {\n        description: SAVE_ATTACHMENT_DESCRIPTION,\n        inputSchema: saveAttachmentInputSchema,\n        run: async (args, signal) =>\n          saveAttachmentToWorkspace(\n            context.workspacePath,\n            saveAttachmentInputSchema.parse(args),\n            listAgentSessionAttachments(context.sessionId),\n            signal\n          )\n      },\n      [MOVE_TO_TRASH_TOOL_NAME]: {\n        description: MOVE_TO_TRASH_DESCRIPTION,\n        inputSchema: moveToTrashInputSchema,\n        run: async (args, signal) =>","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/AssistantFileToolsServer.ts#L41-L77","documentation":"This guard fires after readFileModelOutput() projects a read_file result into an AI-SDK ToolResultOutput. ToolResultOutput is a union (text | image | file), but the MCP handler here can only return a string, so it demands type === 'text'. In practice readFileModelOutput() ALWAYS returns {type:'text', value} (both the success and error branches converge on text), so this throw is a defensive invariant check against a future implementation change, not a normally reachable error.","triggerScenarios":"Reached only if readFileModelOutput() (ReadFileTool.ts:129) is modified to return a non-text variant (image/file) and this call site is not updated. Under the current code path the branch at line 59 is effectively unreachable because isReadFileError and the pagination branch both return {type:'text', value}.","commonSituations":"A developer extends ReadFileTool to return image or file outputs (e.g. adding OCR image passthrough) without revisiting this MCP adapter; or a test stub returns a non-text ToolResultOutput shape (as the test mock at AssistantFileToolsServer.test.ts:21 does, narrowly returning text).","solutions":["Confirm readFileModelOutput still only returns text; if you extended it, update this guard to handle the new output type (e.g. base64-encode image output).","If the invariant must hold, replace the generic Error with a typed assertion so a regression surfaces in CI rather than at runtime.","Search for other call sites of readFileModelOutput and align their assumptions before changing its return type."],"exampleFix":"// before\nconst output = readFileModelOutput(result)\nif (output.type !== 'text') throw new Error('read_file returned an unexpected output type')\nreturn output.value\n\n// after (support image output if readFileModelOutput is extended)\nconst output = readFileModelOutput(result)\nif (output.type === 'text') return output.value\nif (output.type === 'image') return `data:image/png;base64,${output.value}`\nthrow new Error(`Unsupported read_file output type: ${output.type}`)","handlingStrategy":"type-guard","validationCode":"// Before exposing readFileModelOutput to this handler, verify it still only returns text.\nimport { readFileModelOutput } from '@main/ai/tools/adapters/aiSdk/builtin/ReadFileTool'\n// readFileModelOutput always returns { type: 'text', value: string } in current code;\n// if you extend it, add a narrowing wrapper:\nfunction asTextOnly(result: ReadFileResult): string {\n  const out = readFileModelOutput(result)\n  if (out.type !== 'text') {\n    throw new Error(`readFileModelOutput returned non-text: ${out.type}`)\n  }\n  return out.value\n}","typeGuard":"function isTextOutput(out: ToolResultOutput): out is { type: 'text'; value: string } {\n  return out.type === 'text'\n}","tryCatchPattern":null,"preventionTips":["Keep a unit test asserting readFileModelOutput returns only text for all ReadFileResult variants.","When extending ReadFileTool to emit image/file outputs, grep for all call sites of readFileModelOutput and update each.","Use a typed wrapper that narrows to text at the boundary so the MCP handler never sees a non-text type."],"tags":["mcp","invariant","ai-sdk","defensive-check"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}