CherryHQ/cherry-studio · error · Error
read_file returned an unexpected output type
Error message
read_file returned an unexpected output type
What it means
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.
Source
Thrown at src/main/ai/mcp/servers/AssistantFileToolsServer.ts:59
const inputSchema = z.toJSONSchema(handler.inputSchema) as Record<string, unknown>
delete inputSchema.$schema
return { name, description: handler.description, inputSchema: inputSchema as Tool['inputSchema'] }
}
export class AssistantFileToolsServer {
public readonly mcpServer: McpServer
private readonly handlers: Record<string, AssistantFileToolHandler>
constructor(context: AssistantFileToolContext) {
this.handlers = {
[READ_FILE_TOOL_NAME]: {
description: READ_FILE_DESCRIPTION,
inputSchema: readFileInputSchema,
run: async (args, signal) => {
const input = readFileInputSchema.parse(args)
const result = await readFile(input, { attachments: listAgentSessionAttachments(context.sessionId) }, signal)
const output = readFileModelOutput(result)
if (output.type !== 'text') throw new Error('read_file returned an unexpected output type')
return output.value
}
},
[SAVE_ATTACHMENT_TOOL_NAME]: {
description: SAVE_ATTACHMENT_DESCRIPTION,
inputSchema: saveAttachmentInputSchema,
run: async (args, signal) =>
saveAttachmentToWorkspace(
context.workspacePath,
saveAttachmentInputSchema.parse(args),
listAgentSessionAttachments(context.sessionId),
signal
)
},
[MOVE_TO_TRASH_TOOL_NAME]: {
description: MOVE_TO_TRASH_DESCRIPTION,
inputSchema: moveToTrashInputSchema,
run: async (args, signal) =>View on GitHub (pinned to 726446b54c)
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.
Example fix
// before
const output = readFileModelOutput(result)
if (output.type !== 'text') throw new Error('read_file returned an unexpected output type')
return output.value
// after (support image output if readFileModelOutput is extended)
const output = readFileModelOutput(result)
if (output.type === 'text') return output.value
if (output.type === 'image') return `data:image/png;base64,${output.value}`
throw new Error(`Unsupported read_file output type: ${output.type}`) Defensive patterns
Strategy: type-guard
Validate before calling
// Before exposing readFileModelOutput to this handler, verify it still only returns text.
import { readFileModelOutput } from '@main/ai/tools/adapters/aiSdk/builtin/ReadFileTool'
// readFileModelOutput always returns { type: 'text', value: string } in current code;
// if you extend it, add a narrowing wrapper:
function asTextOnly(result: ReadFileResult): string {
const out = readFileModelOutput(result)
if (out.type !== 'text') {
throw new Error(`readFileModelOutput returned non-text: ${out.type}`)
}
return out.value
} Type guard
function isTextOutput(out: ToolResultOutput): out is { type: 'text'; value: string } {
return out.type === 'text'
} Prevention
- 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.
When it happens
Trigger: 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}.
Common situations: 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).
Related errors
- OpenAI-compatible reranking model requires baseURL
- Path traversal detected: target path must be direct child of
- Unsafe DXT entry path (zip-slip): ${name}
- Invalid command: command must be a non-empty string
- Invalid command: command cannot be empty
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/9a4975a465cacd1a.
Report an issue: GitHub.