import { type ReadResourceResult, ReadResourceResultSchema, } from '@modelcontextprotocol/sdk/types.js' import { z } from 'zod/v4' import { ensureConnectedClient } from '../../services/mcp/client.js' import { buildTool, type ToolDef } from '../../Tool.js' import { lazySchema } from '../../utils/lazySchema.js' import { getBinaryBlobSavedMessage, persistBinaryContent, } from '../../utils/mcpOutputStorage.js' import { jsonStringify } from '../../utils/slowOperations.js' import { isOutputLineTruncated } from '../../utils/terminal.js' import { DESCRIPTION, PROMPT } from './prompt.js' import { renderToolResultMessage, renderToolUseMessage, userFacingName, } from './UI.js' export const inputSchema = lazySchema(() => z.object({ server: z.string().describe('The MCP server name'), uri: z.string().describe('The resource URI to read'), }), ) type InputSchema = ReturnType export const outputSchema = lazySchema(() => z.object({ contents: z.array( z.object({ uri: z.string().describe('Resource URI'), mimeType: z.string().optional().describe('MIME type of the content'), text: z.string().optional().describe('Text content of the resource'), blobSavedTo: z .string() .optional() .describe('Path where binary blob content was saved'), }), ), }), ) type OutputSchema = ReturnType export type Output = z.infer export const ReadMcpResourceTool = buildTool({ isConcurrencySafe() { return true }, isReadOnly() { return true }, toAutoClassifierInput(input) { return `${input.server} ${input.uri}` }, shouldDefer: true, name: 'ReadMcpResourceTool', searchHint: 'read a specific MCP resource by URI', maxResultSizeChars: 100_000, async description() { return DESCRIPTION }, async prompt() { return PROMPT }, get inputSchema(): InputSchema { return inputSchema() }, get outputSchema(): OutputSchema { return outputSchema() }, async call(input, { options: { mcpClients } }) { const { server: serverName, uri } = input const client = mcpClients.find(client => client.name === serverName) if (!client) { throw new Error( `Server "${serverName}" not found. Available servers: ${mcpClients.map(c => c.name).join(', ')}`, ) } if (client.type !== 'connected') { throw new Error(`Server "${serverName}" is not connected`) } if (!client.capabilities?.resources) { throw new Error(`Server "${serverName}" does not support resources`) } const connectedClient = await ensureConnectedClient(client) const result = (await connectedClient.client.request( { method: 'resources/read', params: { uri }, }, ReadResourceResultSchema, )) as ReadResourceResult // Intercept any blob fields: decode, write raw bytes to disk with a // mime-derived extension, and replace with a path. Otherwise the base64 // would be stringified straight into the context. const contents = await Promise.all( result.contents.map(async (c, i) => { if ('text' in c) { return { uri: c.uri, mimeType: c.mimeType, text: c.text } } if (!('blob' in c) || typeof c.blob !== 'string') { return { uri: c.uri, mimeType: c.mimeType } } const persistId = `mcp-resource-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}` const persisted = await persistBinaryContent( Buffer.from(c.blob, 'base64'), c.mimeType, persistId, ) if ('error' in persisted) { return { uri: c.uri, mimeType: c.mimeType, text: `Binary content could not be saved to disk: ${persisted.error}`, } } return { uri: c.uri, mimeType: c.mimeType, blobSavedTo: persisted.filepath, text: getBinaryBlobSavedMessage( persisted.filepath, c.mimeType, persisted.size, `[Resource from ${serverName} at ${c.uri}] `, ), } }), ) return { data: { contents }, } }, renderToolUseMessage, userFacingName, renderToolResultMessage, isResultTruncated(output: Output): boolean { return isOutputLineTruncated(jsonStringify(output)) }, mapToolResultToToolResultBlockParam(content, toolUseID) { return { tool_use_id: toolUseID, type: 'tool_result', content: jsonStringify(content), } }, } satisfies ToolDef)