source dump of claude code
at main 158 lines 4.7 kB view raw
1import { 2 type ReadResourceResult, 3 ReadResourceResultSchema, 4} from '@modelcontextprotocol/sdk/types.js' 5import { z } from 'zod/v4' 6import { ensureConnectedClient } from '../../services/mcp/client.js' 7import { buildTool, type ToolDef } from '../../Tool.js' 8import { lazySchema } from '../../utils/lazySchema.js' 9import { 10 getBinaryBlobSavedMessage, 11 persistBinaryContent, 12} from '../../utils/mcpOutputStorage.js' 13import { jsonStringify } from '../../utils/slowOperations.js' 14import { isOutputLineTruncated } from '../../utils/terminal.js' 15import { DESCRIPTION, PROMPT } from './prompt.js' 16import { 17 renderToolResultMessage, 18 renderToolUseMessage, 19 userFacingName, 20} from './UI.js' 21 22export const inputSchema = lazySchema(() => 23 z.object({ 24 server: z.string().describe('The MCP server name'), 25 uri: z.string().describe('The resource URI to read'), 26 }), 27) 28type InputSchema = ReturnType<typeof inputSchema> 29 30export const outputSchema = lazySchema(() => 31 z.object({ 32 contents: z.array( 33 z.object({ 34 uri: z.string().describe('Resource URI'), 35 mimeType: z.string().optional().describe('MIME type of the content'), 36 text: z.string().optional().describe('Text content of the resource'), 37 blobSavedTo: z 38 .string() 39 .optional() 40 .describe('Path where binary blob content was saved'), 41 }), 42 ), 43 }), 44) 45type OutputSchema = ReturnType<typeof outputSchema> 46 47export type Output = z.infer<OutputSchema> 48 49export const ReadMcpResourceTool = buildTool({ 50 isConcurrencySafe() { 51 return true 52 }, 53 isReadOnly() { 54 return true 55 }, 56 toAutoClassifierInput(input) { 57 return `${input.server} ${input.uri}` 58 }, 59 shouldDefer: true, 60 name: 'ReadMcpResourceTool', 61 searchHint: 'read a specific MCP resource by URI', 62 maxResultSizeChars: 100_000, 63 async description() { 64 return DESCRIPTION 65 }, 66 async prompt() { 67 return PROMPT 68 }, 69 get inputSchema(): InputSchema { 70 return inputSchema() 71 }, 72 get outputSchema(): OutputSchema { 73 return outputSchema() 74 }, 75 async call(input, { options: { mcpClients } }) { 76 const { server: serverName, uri } = input 77 78 const client = mcpClients.find(client => client.name === serverName) 79 80 if (!client) { 81 throw new Error( 82 `Server "${serverName}" not found. Available servers: ${mcpClients.map(c => c.name).join(', ')}`, 83 ) 84 } 85 86 if (client.type !== 'connected') { 87 throw new Error(`Server "${serverName}" is not connected`) 88 } 89 90 if (!client.capabilities?.resources) { 91 throw new Error(`Server "${serverName}" does not support resources`) 92 } 93 94 const connectedClient = await ensureConnectedClient(client) 95 const result = (await connectedClient.client.request( 96 { 97 method: 'resources/read', 98 params: { uri }, 99 }, 100 ReadResourceResultSchema, 101 )) as ReadResourceResult 102 103 // Intercept any blob fields: decode, write raw bytes to disk with a 104 // mime-derived extension, and replace with a path. Otherwise the base64 105 // would be stringified straight into the context. 106 const contents = await Promise.all( 107 result.contents.map(async (c, i) => { 108 if ('text' in c) { 109 return { uri: c.uri, mimeType: c.mimeType, text: c.text } 110 } 111 if (!('blob' in c) || typeof c.blob !== 'string') { 112 return { uri: c.uri, mimeType: c.mimeType } 113 } 114 const persistId = `mcp-resource-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}` 115 const persisted = await persistBinaryContent( 116 Buffer.from(c.blob, 'base64'), 117 c.mimeType, 118 persistId, 119 ) 120 if ('error' in persisted) { 121 return { 122 uri: c.uri, 123 mimeType: c.mimeType, 124 text: `Binary content could not be saved to disk: ${persisted.error}`, 125 } 126 } 127 return { 128 uri: c.uri, 129 mimeType: c.mimeType, 130 blobSavedTo: persisted.filepath, 131 text: getBinaryBlobSavedMessage( 132 persisted.filepath, 133 c.mimeType, 134 persisted.size, 135 `[Resource from ${serverName} at ${c.uri}] `, 136 ), 137 } 138 }), 139 ) 140 141 return { 142 data: { contents }, 143 } 144 }, 145 renderToolUseMessage, 146 userFacingName, 147 renderToolResultMessage, 148 isResultTruncated(output: Output): boolean { 149 return isOutputLineTruncated(jsonStringify(output)) 150 }, 151 mapToolResultToToolResultBlockParam(content, toolUseID) { 152 return { 153 tool_use_id: toolUseID, 154 type: 'tool_result', 155 content: jsonStringify(content), 156 } 157 }, 158} satisfies ToolDef<InputSchema, Output>)