| 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<typeof inputSchema> |
|
|
| 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<typeof outputSchema> |
|
|
| export type Output = z.infer<OutputSchema> |
|
|
| 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 |
|
|
| |
| |
| |
| 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<InputSchema, Output>) |
|
|