File size: 4,654 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 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
// 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<InputSchema, Output>)
|