@@ -22,8 +22,11 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
2222import { ModelV2 } from "@opencode-ai/core/model"
2323import { isRecord } from "@/util/record"
2424
25- const LIST_MCP_RESOURCES_TOOL = "list_mcp_resources"
26- const READ_MCP_RESOURCE_TOOL = "read_mcp_resource"
25+ const MCP_RESOURCE_TOOLS = {
26+ list : "list_mcp_resources" ,
27+ listTemplates : "list_mcp_resource_templates" ,
28+ read : "read_mcp_resource" ,
29+ } as const
2730const MAX_MCP_RESOURCE_BLOB_BYTES = 10 * 1024 * 1024
2831const SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES = new Set ( [
2932 "application/pdf" ,
@@ -130,7 +133,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
130133 ( client ) => ! ! client . getServerCapabilities ( ) ?. resources ,
131134 )
132135 if ( hasMcpResourceServer ) {
133- tools [ LIST_MCP_RESOURCES_TOOL ] = tool ( {
136+ tools [ MCP_RESOURCE_TOOLS . list ] = tool ( {
134137 description :
135138 "Lists resources provided by connected MCP servers. Resources provide context such as files, database schemas, or application-specific information." ,
136139 inputSchema : jsonSchema (
@@ -167,7 +170,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
167170 : resourceServers . map ( ( server ) => `mcp:${ server } :*` )
168171 yield * plugin . trigger (
169172 "tool.execute.before" ,
170- { tool : LIST_MCP_RESOURCES_TOOL , sessionID : ctx . sessionID , callID : opts . toolCallId } ,
173+ { tool : MCP_RESOURCE_TOOLS . list , sessionID : ctx . sessionID , callID : opts . toolCallId } ,
171174 { args } ,
172175 )
173176 yield * ctx . ask ( {
@@ -200,7 +203,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
200203 }
201204 yield * plugin . trigger (
202205 "tool.execute.after" ,
203- { tool : LIST_MCP_RESOURCES_TOOL , sessionID : ctx . sessionID , callID : opts . toolCallId , args } ,
206+ { tool : MCP_RESOURCE_TOOLS . list , sessionID : ctx . sessionID , callID : opts . toolCallId , args } ,
204207 output ,
205208 )
206209 if ( opts . abortSignal ?. aborted ) {
@@ -212,7 +215,89 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
212215 } ,
213216 } )
214217
215- tools [ READ_MCP_RESOURCE_TOOL ] = tool ( {
218+ tools [ MCP_RESOURCE_TOOLS . listTemplates ] = tool ( {
219+ description :
220+ "Lists resource templates provided by connected MCP servers. Resource templates are parameterized resources that can be read after filling in their URI template." ,
221+ inputSchema : jsonSchema (
222+ ProviderTransform . schema ( input . model , {
223+ type : "object" ,
224+ properties : {
225+ server : {
226+ type : "string" ,
227+ description : "Optional MCP server name. When omitted, lists resource templates from every connected server." ,
228+ } ,
229+ } ,
230+ additionalProperties : false ,
231+ } ) ,
232+ ) ,
233+ execute ( args , opts ) {
234+ return run . promise (
235+ Effect . gen ( function * ( ) {
236+ const parsed = parseListMcpResourcesArgs ( args )
237+ const ctx = context ( toRecord ( args ) , opts )
238+ const clients = yield * mcp . clients ( )
239+ const resourceServers = Object . entries ( clients )
240+ . filter ( ( entry ) => ! ! entry [ 1 ] . getServerCapabilities ( ) ?. resources )
241+ . map ( ( entry ) => entry [ 0 ] )
242+ . sort ( ( a , b ) => a . localeCompare ( b ) )
243+ if ( parsed . server && ! resourceServers . includes ( parsed . server ) ) {
244+ throw new Error (
245+ resourceServers . length === 0
246+ ? `MCP server "${ parsed . server } " does not support resources`
247+ : `MCP server "${ parsed . server } " does not support resources. Available resource servers: ${ resourceServers . join ( ", " ) } ` ,
248+ )
249+ }
250+ const permissionPatterns = parsed . server
251+ ? [ `mcp:${ parsed . server } :*` ]
252+ : resourceServers . map ( ( server ) => `mcp:${ server } :*` )
253+ yield * plugin . trigger (
254+ "tool.execute.before" ,
255+ { tool : MCP_RESOURCE_TOOLS . listTemplates , sessionID : ctx . sessionID , callID : opts . toolCallId } ,
256+ { args } ,
257+ )
258+ yield * ctx . ask ( {
259+ permission : "read" ,
260+ metadata : parsed . server ? { server : parsed . server } : { } ,
261+ patterns : permissionPatterns ,
262+ always : permissionPatterns ,
263+ } )
264+
265+ const templates = Object . values ( yield * mcp . resourceTemplates ( parsed . server ) )
266+ const filtered = templates
267+ . filter ( ( template ) => ! parsed . server || template . client === parsed . server )
268+ . toSorted ( ( a , b ) =>
269+ ( a . client + "\u0000" + a . name + "\u0000" + a . uriTemplate ) . localeCompare (
270+ b . client + "\u0000" + b . name + "\u0000" + b . uriTemplate ,
271+ ) ,
272+ )
273+ const content = JSON . stringify ( { resourceTemplates : filtered . map ( formatMcpResourceTemplate ) } , null , 2 )
274+ const truncated = yield * truncate . output ( content , { } , input . agent )
275+ const output = {
276+ title : parsed . server ? `MCP resource templates: ${ parsed . server } ` : "MCP resource templates" ,
277+ metadata : {
278+ count : filtered . length ,
279+ servers : resourceServers ,
280+ ...( parsed . server ? { server : parsed . server } : { } ) ,
281+ truncated : truncated . truncated ,
282+ ...( truncated . truncated && { outputPath : truncated . outputPath } ) ,
283+ } ,
284+ output : truncated . content ,
285+ }
286+ yield * plugin . trigger (
287+ "tool.execute.after" ,
288+ { tool : MCP_RESOURCE_TOOLS . listTemplates , sessionID : ctx . sessionID , callID : opts . toolCallId , args } ,
289+ output ,
290+ )
291+ if ( opts . abortSignal ?. aborted ) {
292+ yield * input . processor . completeToolCall ( opts . toolCallId , output )
293+ }
294+ return output
295+ } ) ,
296+ )
297+ } ,
298+ } )
299+
300+ tools [ MCP_RESOURCE_TOOLS . read ] = tool ( {
216301 description :
217302 "Read a specific resource from an MCP server using the server name and resource URI. The URI is an MCP identifier and does not need to be a file URL." ,
218303 inputSchema : jsonSchema (
@@ -247,7 +332,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
247332 }
248333 yield * plugin . trigger (
249334 "tool.execute.before" ,
250- { tool : READ_MCP_RESOURCE_TOOL , sessionID : ctx . sessionID , callID : opts . toolCallId } ,
335+ { tool : MCP_RESOURCE_TOOLS . read , sessionID : ctx . sessionID , callID : opts . toolCallId } ,
251336 { args } ,
252337 )
253338 yield * ctx . ask ( {
@@ -282,7 +367,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
282367 }
283368 yield * plugin . trigger (
284369 "tool.execute.after" ,
285- { tool : READ_MCP_RESOURCE_TOOL , sessionID : ctx . sessionID , callID : opts . toolCallId , args } ,
370+ { tool : MCP_RESOURCE_TOOLS . read , sessionID : ctx . sessionID , callID : opts . toolCallId , args } ,
286371 output ,
287372 )
288373 if ( opts . abortSignal ?. aborted ) {
@@ -432,6 +517,11 @@ function formatMcpResource(resource: MCP.Resource) {
432517 return { ...result , server : resource . client }
433518}
434519
520+ function formatMcpResourceTemplate ( template : Record < string , unknown > & { client : string } ) {
521+ const result = Object . fromEntries ( Object . entries ( template ) . filter ( ( entry ) => entry [ 0 ] !== "client" ) )
522+ return { ...result , server : template . client }
523+ }
524+
435525function formatMcpResourceContent ( server : string , uri : string , content : { contents : unknown } ) {
436526 const items = ( Array . isArray ( content . contents ) ? content . contents : [ content . contents ] ) . filter ( isRecord )
437527 const text : string [ ] = [ ]
0 commit comments