init claude-code
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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>)
|
||||
Reference in New Issue
Block a user