forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReadMcpResourceTool.ts
More file actions
158 lines (147 loc) · 4.54 KB
/
Copy pathReadMcpResourceTool.ts
File metadata and controls
158 lines (147 loc) · 4.54 KB
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
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>)