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

  1. 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).
  2. If the invariant must hold, replace the generic Error with a typed assertion so a regression surfaces in CI rather than at runtime.
  3. 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

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


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/9a4975a465cacd1a. Report an issue: GitHub.