2026-05-18 19:50:31 +00:00
import { Agent } from "@/agent/agent"
2026-06-03 02:42:13 +00:00
import { SessionV1 } from "@opencode-ai/core/v1/session"
2026-05-18 19:50:31 +00:00
import { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { MCP } from "@/mcp"
import { Permission } from "@/permission"
import { Tool } from "@/tool/tool"
import { ToolJsonSchema } from "@/tool/json-schema"
import { ToolRegistry } from "@/tool/registry"
import { Truncate } from "@/tool/truncate"
2026-05-31 01:08:38 +00:00
2026-05-18 19:50:31 +00:00
import { Plugin } from "@/plugin"
import type { TaskPromptOps } from "@/tool/task"
import { type Tool as AITool , tool , jsonSchema , type ToolExecutionOptions , asSchema } from "ai"
import { Effect } from "effect"
import { MessageV2 } from "./message-v2"
2026-06-01 05:00:40 +00:00
import { Session } from "./session"
2026-05-18 19:50:31 +00:00
import { SessionProcessor } from "./processor"
import { PartID } from "./schema"
import { EffectBridge } from "@/effect/bridge"
2026-05-31 01:08:38 +00:00
import { ProviderV2 } from "@opencode-ai/core/provider"
2026-06-04 06:57:43 +00:00
import { ModelV2 } from "@opencode-ai/core/model"
2026-06-23 08:19:42 +00:00
import { isRecord } from "@/util/record"
2026-06-23 20:29:12 +00:00
const MCP_RESOURCE_TOOLS = {
list : "list_mcp_resources" ,
listTemplates : "list_mcp_resource_templates" ,
read : "read_mcp_resource" ,
} as const
2026-06-23 08:19:42 +00:00
const MAX_MCP_RESOURCE_BLOB_BYTES = 10 * 1024 * 1024
const SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES = new Set ( [
"application/pdf" ,
"image/gif" ,
"image/jpeg" ,
"image/png" ,
"image/webp" ,
] )
2026-05-18 19:50:31 +00:00
export const resolve = Effect . fn ( "SessionTools.resolve" ) ( function * ( input : {
agent : Agent.Info
model : Provider.Model
session : Session.Info
processor : Pick < SessionProcessor.Handle , " message " | " updateToolCall " | " completeToolCall " >
bypassAgentCheck : boolean
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
2026-05-18 19:50:31 +00:00
promptOps : TaskPromptOps
} ) {
const tools : Record < string , AITool > = { }
const run = yield * EffectBridge . make ( )
const plugin = yield * Plugin . Service
const permission = yield * Permission . Service
const registry = yield * ToolRegistry . Service
const mcp = yield * MCP . Service
const truncate = yield * Truncate . Service
const context = ( args : Record < string , unknown > , options : ToolExecutionOptions ) : Tool . Context = > ( {
sessionID : input.session.id ,
abort : options.abortSignal ! ,
messageID : input.processor.message.id ,
callID : options.toolCallId ,
extra : { model : input.model , bypassAgentCheck : input.bypassAgentCheck , promptOps : input.promptOps } ,
agent : input.agent.name ,
messages : input.messages ,
metadata : ( val ) = >
input . processor . updateToolCall ( options . toolCallId , ( match ) = > {
if ( ! [ "running" , "pending" ] . includes ( match . state . status ) ) return match
return {
. . . match ,
state : {
title : val.title ,
metadata : val.metadata ,
status : "running" ,
input : args ,
time : { start : Date.now ( ) } ,
} ,
}
} ) ,
ask : ( req ) = >
permission
. ask ( {
. . . req ,
sessionID : input.session.id ,
tool : { messageID : input.processor.message.id , callID : options.toolCallId } ,
ruleset : Permission.merge ( input . agent . permission , input . session . permission ? ? [ ] ) ,
} )
. pipe ( Effect . orDie ) ,
} )
for ( const item of yield * registry . tools ( {
2026-06-04 06:57:43 +00:00
modelID : ModelV2.ID.make ( input . model . api . id ) ,
2026-05-18 19:50:31 +00:00
providerID : input.model.providerID ,
agent : input.agent ,
} ) ) {
const schema = ProviderTransform . schema ( input . model , ToolJsonSchema . fromTool ( item ) )
tools [ item . id ] = tool ( {
description : item.description ,
inputSchema : jsonSchema ( schema ) ,
execute ( args , options ) {
return run . promise (
Effect . gen ( function * ( ) {
const ctx = context ( args , options )
yield * plugin . trigger (
"tool.execute.before" ,
{ tool : item.id , sessionID : ctx.sessionID , callID : ctx.callID } ,
{ args } ,
)
const result = yield * item . execute ( args , ctx )
const output = {
. . . result ,
attachments : result.attachments?.map ( ( attachment ) = > ( {
. . . attachment ,
id : PartID.ascending ( ) ,
sessionID : ctx.sessionID ,
messageID : input.processor.message.id ,
} ) ) ,
}
yield * plugin . trigger (
"tool.execute.after" ,
{ tool : item.id , sessionID : ctx.sessionID , callID : ctx.callID , args } ,
output ,
)
if ( options . abortSignal ? . aborted ) {
yield * input . processor . completeToolCall ( options . toolCallId , output )
}
return output
} ) ,
)
} ,
} )
}
2026-06-23 08:19:42 +00:00
const hasMcpResourceServer = Object . values ( yield * mcp . clients ( ) ) . some (
( client ) = > ! ! client . getServerCapabilities ( ) ? . resources ,
)
if ( hasMcpResourceServer ) {
2026-06-23 20:29:12 +00:00
tools [ MCP_RESOURCE_TOOLS . list ] = tool ( {
2026-06-23 08:19:42 +00:00
description :
"Lists resources provided by connected MCP servers. Resources provide context such as files, database schemas, or application-specific information." ,
inputSchema : jsonSchema (
ProviderTransform . schema ( input . model , {
type : "object" ,
properties : {
server : {
type : "string" ,
description : "Optional MCP server name. When omitted, lists resources from every connected server." ,
} ,
} ,
additionalProperties : false ,
} ) ,
) ,
execute ( args , opts ) {
return run . promise (
Effect . gen ( function * ( ) {
const parsed = parseListMcpResourcesArgs ( args )
const ctx = context ( toRecord ( args ) , opts )
const clients = yield * mcp . clients ( )
const resourceServers = Object . entries ( clients )
. filter ( ( entry ) = > ! ! entry [ 1 ] . getServerCapabilities ( ) ? . resources )
. map ( ( entry ) = > entry [ 0 ] )
. sort ( ( a , b ) = > a . localeCompare ( b ) )
if ( parsed . server && ! resourceServers . includes ( parsed . server ) ) {
throw new Error (
resourceServers . length === 0
? ` MCP server " ${ parsed . server } " does not support resources `
: ` MCP server " ${ parsed . server } " does not support resources. Available resource servers: ${ resourceServers . join ( ", " ) } ` ,
)
}
const permissionPatterns = parsed . server
? [ ` mcp: ${ parsed . server } :* ` ]
: resourceServers . map ( ( server ) = > ` mcp: ${ server } :* ` )
yield * plugin . trigger (
"tool.execute.before" ,
2026-06-23 20:29:12 +00:00
{ tool : MCP_RESOURCE_TOOLS.list , sessionID : ctx.sessionID , callID : opts.toolCallId } ,
2026-06-23 08:19:42 +00:00
{ args } ,
)
yield * ctx . ask ( {
permission : "read" ,
metadata : parsed.server ? { server : parsed.server } : { } ,
patterns : permissionPatterns ,
always : permissionPatterns ,
} )
const resources = Object . values ( yield * mcp . resources ( parsed . server ) )
const filtered = resources
. filter ( ( resource ) = > ! parsed . server || resource . client === parsed . server )
. toSorted ( ( a , b ) = >
( a . client + "\u0000" + a . name + "\u0000" + a . uri ) . localeCompare (
b . client + "\u0000" + b . name + "\u0000" + b . uri ,
) ,
)
const content = JSON . stringify ( { resources : filtered.map ( formatMcpResource ) } , null , 2 )
const truncated = yield * truncate . output ( content , { } , input . agent )
const output = {
title : parsed.server ? ` MCP resources: ${ parsed . server } ` : "MCP resources" ,
metadata : {
count : filtered.length ,
servers : resourceServers ,
. . . ( parsed . server ? { server : parsed.server } : { } ) ,
truncated : truncated.truncated ,
. . . ( truncated . truncated && { outputPath : truncated.outputPath } ) ,
} ,
output : truncated.content ,
}
yield * plugin . trigger (
"tool.execute.after" ,
2026-06-23 20:29:12 +00:00
{ tool : MCP_RESOURCE_TOOLS.list , sessionID : ctx.sessionID , callID : opts.toolCallId , args } ,
2026-06-23 08:19:42 +00:00
output ,
)
if ( opts . abortSignal ? . aborted ) {
yield * input . processor . completeToolCall ( opts . toolCallId , output )
}
return output
} ) ,
)
} ,
} )
2026-06-23 20:29:12 +00:00
tools [ MCP_RESOURCE_TOOLS . listTemplates ] = tool ( {
description :
"Lists resource templates provided by connected MCP servers. Resource templates are parameterized resources that can be read after filling in their URI template." ,
inputSchema : jsonSchema (
ProviderTransform . schema ( input . model , {
type : "object" ,
properties : {
server : {
type : "string" ,
2026-06-23 20:31:13 +00:00
description :
"Optional MCP server name. When omitted, lists resource templates from every connected server." ,
2026-06-23 20:29:12 +00:00
} ,
} ,
additionalProperties : false ,
} ) ,
) ,
execute ( args , opts ) {
return run . promise (
Effect . gen ( function * ( ) {
const parsed = parseListMcpResourcesArgs ( args )
const ctx = context ( toRecord ( args ) , opts )
const clients = yield * mcp . clients ( )
const resourceServers = Object . entries ( clients )
. filter ( ( entry ) = > ! ! entry [ 1 ] . getServerCapabilities ( ) ? . resources )
. map ( ( entry ) = > entry [ 0 ] )
. sort ( ( a , b ) = > a . localeCompare ( b ) )
if ( parsed . server && ! resourceServers . includes ( parsed . server ) ) {
throw new Error (
resourceServers . length === 0
? ` MCP server " ${ parsed . server } " does not support resources `
: ` MCP server " ${ parsed . server } " does not support resources. Available resource servers: ${ resourceServers . join ( ", " ) } ` ,
)
}
const permissionPatterns = parsed . server
? [ ` mcp: ${ parsed . server } :* ` ]
: resourceServers . map ( ( server ) = > ` mcp: ${ server } :* ` )
yield * plugin . trigger (
"tool.execute.before" ,
{ tool : MCP_RESOURCE_TOOLS.listTemplates , sessionID : ctx.sessionID , callID : opts.toolCallId } ,
{ args } ,
)
yield * ctx . ask ( {
permission : "read" ,
metadata : parsed.server ? { server : parsed.server } : { } ,
patterns : permissionPatterns ,
always : permissionPatterns ,
} )
const templates = Object . values ( yield * mcp . resourceTemplates ( parsed . server ) )
const filtered = templates
. filter ( ( template ) = > ! parsed . server || template . client === parsed . server )
. toSorted ( ( a , b ) = >
( a . client + "\u0000" + a . name + "\u0000" + a . uriTemplate ) . localeCompare (
b . client + "\u0000" + b . name + "\u0000" + b . uriTemplate ,
) ,
)
const content = JSON . stringify ( { resourceTemplates : filtered.map ( formatMcpResourceTemplate ) } , null , 2 )
const truncated = yield * truncate . output ( content , { } , input . agent )
const output = {
title : parsed.server ? ` MCP resource templates: ${ parsed . server } ` : "MCP resource templates" ,
metadata : {
count : filtered.length ,
servers : resourceServers ,
. . . ( parsed . server ? { server : parsed.server } : { } ) ,
truncated : truncated.truncated ,
. . . ( truncated . truncated && { outputPath : truncated.outputPath } ) ,
} ,
output : truncated.content ,
}
yield * plugin . trigger (
"tool.execute.after" ,
{ tool : MCP_RESOURCE_TOOLS.listTemplates , sessionID : ctx.sessionID , callID : opts.toolCallId , args } ,
output ,
)
if ( opts . abortSignal ? . aborted ) {
yield * input . processor . completeToolCall ( opts . toolCallId , output )
}
return output
} ) ,
)
} ,
} )
tools [ MCP_RESOURCE_TOOLS . read ] = tool ( {
2026-06-23 08:19:42 +00:00
description :
"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." ,
inputSchema : jsonSchema (
ProviderTransform . schema ( input . model , {
type : "object" ,
properties : {
server : {
type : "string" ,
description : "MCP server name exactly as returned by list_mcp_resources." ,
} ,
uri : {
type : "string" ,
description : "Resource URI to read. Use the exact URI string returned by list_mcp_resources." ,
} ,
} ,
required : [ "server" , "uri" ] ,
additionalProperties : false ,
} ) ,
) ,
execute ( args , opts ) {
return run . promise (
Effect . gen ( function * ( ) {
const parsed = parseReadMcpResourceArgs ( args )
const ctx = context ( toRecord ( args ) , opts )
const clients = yield * mcp . clients ( )
const client = clients [ parsed . server ]
if ( ! client ) {
throw new Error ( ` MCP server " ${ parsed . server } " is not connected ` )
}
if ( ! client . getServerCapabilities ( ) ? . resources ) {
throw new Error ( ` MCP server " ${ parsed . server } " does not support resources ` )
}
yield * plugin . trigger (
"tool.execute.before" ,
2026-06-23 20:29:12 +00:00
{ tool : MCP_RESOURCE_TOOLS.read , sessionID : ctx.sessionID , callID : opts.toolCallId } ,
2026-06-23 08:19:42 +00:00
{ args } ,
)
yield * ctx . ask ( {
permission : "read" ,
metadata : { server : parsed.server , uri : parsed.uri } ,
patterns : [ ` mcp: ${ parsed . server } : ${ parsed . uri } ` ] ,
always : [ ` mcp: ${ parsed . server } :* ` ] ,
} )
const content = yield * mcp . readResource ( parsed . server , parsed . uri )
if ( ! content ) throw new Error ( ` Failed to read MCP resource: ${ parsed . server } / ${ parsed . uri } ` )
const formatted = formatMcpResourceContent ( parsed . server , parsed . uri , content )
const truncated = yield * truncate . output ( formatted . text , { } , input . agent )
const output = {
title : ` MCP resource: ${ parsed . uri } ` ,
metadata : {
server : parsed.server ,
uri : parsed.uri ,
contents : formatted.contents ,
attachments : formatted.attachments.length ,
truncated : truncated.truncated ,
. . . ( truncated . truncated && { outputPath : truncated.outputPath } ) ,
} ,
output : truncated.content ,
attachments : formatted.attachments.map ( ( attachment ) = > ( {
. . . attachment ,
id : PartID.ascending ( ) ,
sessionID : ctx.sessionID ,
messageID : input.processor.message.id ,
} ) ) ,
}
yield * plugin . trigger (
"tool.execute.after" ,
2026-06-23 20:29:12 +00:00
{ tool : MCP_RESOURCE_TOOLS.read , sessionID : ctx.sessionID , callID : opts.toolCallId , args } ,
2026-06-23 08:19:42 +00:00
output ,
)
if ( opts . abortSignal ? . aborted ) {
yield * input . processor . completeToolCall ( opts . toolCallId , output )
}
return output
} ) ,
)
} ,
} )
}
2026-05-18 19:50:31 +00:00
for ( const [ key , item ] of Object . entries ( yield * mcp . tools ( ) ) ) {
const execute = item . execute
if ( ! execute ) continue
const schema = yield * Effect . promise ( ( ) = > Promise . resolve ( asSchema ( item . inputSchema ) . jsonSchema ) )
2026-06-16 14:27:20 +00:00
const transformed = ProviderTransform . schema ( input . model , { . . . schema , properties : schema.properties ? ? { } } )
2026-05-18 19:50:31 +00:00
item . inputSchema = jsonSchema ( transformed )
item . execute = ( args , opts ) = >
run . promise (
Effect . gen ( function * ( ) {
const ctx = context ( args , opts )
yield * plugin . trigger (
"tool.execute.before" ,
{ tool : key , sessionID : ctx.sessionID , callID : opts.toolCallId } ,
{ args } ,
)
const result : Awaited < ReturnType < NonNullable < typeof execute > >> = yield * Effect . gen ( function * ( ) {
yield * ctx . ask ( { permission : key , metadata : { } , patterns : [ "*" ] , always : [ "*" ] } )
return yield * Effect . promise ( ( ) = > execute ( args , opts ) )
} ) . pipe (
Effect . withSpan ( "Tool.execute" , {
attributes : {
"tool.name" : key ,
"tool.call_id" : opts . toolCallId ,
"session.id" : ctx . sessionID ,
"message.id" : input . processor . message . id ,
} ,
} ) ,
)
yield * plugin . trigger (
"tool.execute.after" ,
{ tool : key , sessionID : ctx.sessionID , callID : opts.toolCallId , args } ,
result ,
)
const textParts : string [ ] = [ ]
2026-06-03 02:42:13 +00:00
const attachments : Omit < SessionV1.FilePart , " id " | " sessionID " | " messageID " > [ ] = [ ]
2026-05-18 19:50:31 +00:00
for ( const contentItem of result . content ) {
if ( contentItem . type === "text" ) textParts . push ( contentItem . text )
else if ( contentItem . type === "image" ) {
attachments . push ( {
type : "file" ,
mime : contentItem.mimeType ,
url : ` data: ${ contentItem . mimeType } ;base64, ${ contentItem . data } ` ,
} )
} else if ( contentItem . type === "resource" ) {
const { resource } = contentItem
if ( resource . text ) textParts . push ( resource . text )
if ( resource . blob ) {
2026-06-23 08:19:42 +00:00
const mime = resource . mimeType ? ? "application/octet-stream"
const size = base64Size ( resource . blob )
if ( ! SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES . has ( mime ) ) {
textParts . push (
` [Binary MCP resource omitted: ${ resource . uri } ( ${ mime } , ${ formatBytes ( size ) } ) is not a supported attachment type] ` ,
)
continue
}
if ( size > MAX_MCP_RESOURCE_BLOB_BYTES ) {
textParts . push (
` [Binary MCP resource omitted: ${ resource . uri } ( ${ mime } , ${ formatBytes ( size ) } ) exceeds ${ formatBytes ( MAX_MCP_RESOURCE_BLOB_BYTES ) } ] ` ,
)
continue
}
2026-05-18 19:50:31 +00:00
attachments . push ( {
type : "file" ,
2026-06-23 08:19:42 +00:00
mime ,
url : ` data: ${ mime } ;base64, ${ resource . blob } ` ,
2026-05-18 19:50:31 +00:00
filename : resource.uri ,
} )
}
}
}
const truncated = yield * truncate . output ( textParts . join ( "\n\n" ) , { } , input . agent )
const metadata = {
. . . result . metadata ,
truncated : truncated.truncated ,
. . . ( truncated . truncated && { outputPath : truncated.outputPath } ) ,
}
const output = {
title : "" ,
metadata ,
output : truncated.content ,
attachments : attachments.map ( ( attachment ) = > ( {
. . . attachment ,
id : PartID.ascending ( ) ,
sessionID : ctx.sessionID ,
messageID : input.processor.message.id ,
} ) ) ,
content : result.content ,
}
if ( opts . abortSignal ? . aborted ) {
yield * input . processor . completeToolCall ( opts . toolCallId , output )
}
return output
} ) ,
)
tools [ key ] = item
}
return tools
} )
2026-06-23 08:19:42 +00:00
function toRecord ( value : unknown ) {
if ( isRecord ( value ) ) return value
return { }
}
function parseListMcpResourcesArgs ( value : unknown ) {
const args = toRecord ( value )
return { server : optionalString ( args , "server" ) }
}
function parseReadMcpResourceArgs ( value : unknown ) {
const args = toRecord ( value )
return { server : requiredString ( args , "server" ) , uri : requiredString ( args , "uri" ) }
}
function optionalString ( args : Record < string , unknown > , key : string ) {
const value = args [ key ]
if ( value === undefined || value === null || value === "" ) return undefined
if ( typeof value !== "string" ) throw new Error ( ` ${ key } must be a string ` )
return value
}
function requiredString ( args : Record < string , unknown > , key : string ) {
const value = optionalString ( args , key )
if ( value ) return value
throw new Error ( ` ${ key } is required ` )
}
function formatMcpResource ( resource : MCP.Resource ) {
const result = Object . fromEntries ( Object . entries ( resource ) . filter ( ( entry ) = > entry [ 0 ] !== "client" ) )
return { . . . result , server : resource.client }
}
2026-06-23 20:29:12 +00:00
function formatMcpResourceTemplate ( template : Record < string , unknown > & { client : string } ) {
const result = Object . fromEntries ( Object . entries ( template ) . filter ( ( entry ) = > entry [ 0 ] !== "client" ) )
return { . . . result , server : template.client }
}
2026-06-23 08:19:42 +00:00
function formatMcpResourceContent ( server : string , uri : string , content : { contents : unknown } ) {
const items = ( Array . isArray ( content . contents ) ? content . contents : [ content . contents ] ) . filter ( isRecord )
const text : string [ ] = [ ]
const attachments : Omit < SessionV1.FilePart , " id " | " sessionID " | " messageID " > [ ] = [ ]
for ( const item of items ) {
const itemUri = typeof item . uri === "string" ? item.uri : uri
const mime = typeof item . mimeType === "string" ? item . mimeType : "application/octet-stream"
if ( typeof item . text === "string" ) {
text . push ( ` Resource: ${ itemUri } \ nMIME: ${ mime } \ n ${ item . text } ` )
continue
}
if ( typeof item . blob === "string" ) {
const size = base64Size ( item . blob )
if ( ! SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES . has ( mime ) ) {
text . push (
` [Binary MCP resource omitted: ${ itemUri } ( ${ mime } , ${ formatBytes ( size ) } ) is not a supported attachment type] ` ,
)
continue
}
if ( size > MAX_MCP_RESOURCE_BLOB_BYTES ) {
text . push (
` [Binary MCP resource omitted: ${ itemUri } ( ${ mime } , ${ formatBytes ( size ) } ) exceeds ${ formatBytes ( MAX_MCP_RESOURCE_BLOB_BYTES ) } ] ` ,
)
continue
}
text . push ( ` [Binary MCP resource attached: ${ itemUri } ( ${ mime } )] ` )
attachments . push ( {
type : "file" ,
mime ,
url : ` data: ${ mime } ;base64, ${ item . blob } ` ,
filename : itemUri ,
} )
continue
}
text . push ( ` [MCP resource content without text or blob: ${ itemUri } ] ` )
}
return {
contents : items.length ,
attachments ,
text : text.join ( "\n\n" ) || ` MCP resource ${ uri } from ${ server } returned no contents. ` ,
}
}
function base64Size ( value : string ) {
const trimmed = value . replace ( /\s/g , "" )
const padding = trimmed . endsWith ( "==" ) ? 2 : trimmed.endsWith ( "=" ) ? 1 : 0
return Math . max ( 0 , Math . floor ( ( trimmed . length * 3 ) / 4 ) - padding )
}
function formatBytes ( value : number ) {
if ( value < 1024 ) return ` ${ value } B `
if ( value < 1024 * 1024 ) return ` ${ Math . ceil ( value / 1024 ) } KB `
return ` ${ Math . ceil ( value / ( 1024 * 1024 ) ) } MB `
}
2026-05-18 19:50:31 +00:00
export * as SessionTools from "./tools"