1- import { type Tool } from "ai"
2- import { experimental_createMCPClient } from "@ai- sdk/mcp "
1+ import { dynamicTool , type Tool , jsonSchema , type JSONSchema7 } from "ai"
2+ import { Client } from "@modelcontextprotocol/ sdk/client/index.js "
33import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
44import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
55import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
66import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
7+ import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
78import { Config } from "../config/config"
89import { Log } from "../util/log"
910import { NamedError } from "@opencode-ai/util/error"
1011import z from "zod/v4"
1112import { Instance } from "../project/instance"
13+ import { Installation } from "../installation"
1214import { withTimeout } from "@/util/timeout"
1315import { McpOAuthProvider } from "./oauth-provider"
1416import { McpOAuthCallback } from "./oauth-callback"
@@ -25,7 +27,7 @@ export namespace MCP {
2527 } ) ,
2628 )
2729
28- type Client = Awaited < ReturnType < typeof experimental_createMCPClient > >
30+ type MCPClient = Client
2931
3032 export const Status = z
3133 . discriminatedUnion ( "status" , [
@@ -71,7 +73,30 @@ export namespace MCP {
7173 ref : "MCPStatus" ,
7274 } )
7375 export type Status = z . infer < typeof Status >
74- type MCPClient = Awaited < ReturnType < typeof experimental_createMCPClient > >
76+
77+ // Convert MCP tool definition to AI SDK Tool type
78+ function convertMcpTool ( mcpTool : MCPToolDef , client : MCPClient ) : Tool {
79+ const inputSchema = mcpTool . inputSchema
80+
81+ // Spread first, then override type to ensure it's always "object"
82+ const schema : JSONSchema7 = {
83+ ...( inputSchema as JSONSchema7 ) ,
84+ type : "object" ,
85+ properties : ( inputSchema . properties ?? { } ) as JSONSchema7 [ "properties" ] ,
86+ additionalProperties : false ,
87+ }
88+
89+ return dynamicTool ( {
90+ description : mcpTool . description ?? "" ,
91+ inputSchema : jsonSchema ( schema ) ,
92+ execute : async ( args : unknown ) => {
93+ return client . callTool ( {
94+ name : mcpTool . name ,
95+ arguments : args as Record < string , unknown > ,
96+ } )
97+ } ,
98+ } )
99+ }
75100
76101 // Store transports for OAuth servers to allow finishing auth
77102 type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
@@ -81,7 +106,7 @@ export namespace MCP {
81106 async ( ) => {
82107 const cfg = await Config . get ( )
83108 const config = cfg . mcp ?? { }
84- const clients : Record < string , Client > = { }
109+ const clients : Record < string , MCPClient > = { }
85110 const status : Record < string , Status > = { }
86111
87112 await Promise . all (
@@ -204,10 +229,12 @@ export namespace MCP {
204229 let lastError : Error | undefined
205230 for ( const { name, transport } of transports ) {
206231 try {
207- mcpClient = await experimental_createMCPClient ( {
232+ const client = new Client ( {
208233 name : "opencode" ,
209- transport ,
234+ version : Installation . VERSION ,
210235 } )
236+ await client . connect ( transport )
237+ mcpClient = client
211238 log . info ( "connected" , { key, transport : name } )
212239 status = { status : "connected" }
213240 break
@@ -248,36 +275,38 @@ export namespace MCP {
248275
249276 if ( mcp . type === "local" ) {
250277 const [ cmd , ...args ] = mcp . command
251- await experimental_createMCPClient ( {
252- name : "opencode" ,
253- transport : new StdioClientTransport ( {
254- stderr : "ignore" ,
255- command : cmd ,
256- args,
257- env : {
258- ...process . env ,
259- ...( cmd === "opencode" ? { BUN_BE_BUN : "1" } : { } ) ,
260- ...mcp . environment ,
261- } ,
262- } ) ,
278+ const transport = new StdioClientTransport ( {
279+ stderr : "ignore" ,
280+ command : cmd ,
281+ args,
282+ env : {
283+ ...process . env ,
284+ ...( cmd === "opencode" ? { BUN_BE_BUN : "1" } : { } ) ,
285+ ...mcp . environment ,
286+ } ,
263287 } )
264- . then ( ( client ) => {
265- mcpClient = client
266- status = {
267- status : "connected " ,
268- }
288+
289+ try {
290+ const client = new Client ( {
291+ name : "opencode " ,
292+ version : Installation . VERSION ,
269293 } )
270- . catch ( ( error ) => {
271- log . error ( "local mcp startup failed" , {
272- key ,
273- command : mcp . command ,
274- error : error instanceof Error ? error . message : String ( error ) ,
275- } )
276- status = {
277- status : "failed" as const ,
278- error : error instanceof Error ? error . message : String ( error ) ,
279- }
294+ await client . connect ( transport )
295+ mcpClient = client
296+ status = {
297+ status : "connected" ,
298+ }
299+ } catch ( error ) {
300+ log . error ( "local mcp startup failed" , {
301+ key ,
302+ command : mcp . command ,
303+ error : error instanceof Error ? error . message : String ( error ) ,
280304 } )
305+ status = {
306+ status : "failed" as const ,
307+ error : error instanceof Error ? error . message : String ( error ) ,
308+ }
309+ }
281310 }
282311
283312 if ( ! status ) {
@@ -294,7 +323,7 @@ export namespace MCP {
294323 }
295324 }
296325
297- const result = await withTimeout ( mcpClient . tools ( ) , mcp . timeout ?? 5000 ) . catch ( ( err ) => {
326+ const result = await withTimeout ( mcpClient . listTools ( ) , mcp . timeout ?? 5000 ) . catch ( ( err ) => {
298327 log . error ( "failed to get tools from client" , { key, error : err } )
299328 return undefined
300329 } )
@@ -317,7 +346,7 @@ export namespace MCP {
317346 }
318347 }
319348
320- log . info ( "create() successfully created client" , { key, toolCount : Object . keys ( result ) . length } )
349+ log . info ( "create() successfully created client" , { key, toolCount : result . tools . length } )
321350 return {
322351 mcpClient,
323352 status,
@@ -392,22 +421,23 @@ export namespace MCP {
392421 continue
393422 }
394423
395- const tools = await client . tools ( ) . catch ( ( e ) => {
424+ const toolsResult = await client . listTools ( ) . catch ( ( e ) => {
396425 log . error ( "failed to get tools" , { clientName, error : e . message } )
397426 const failedStatus = {
398427 status : "failed" as const ,
399428 error : e instanceof Error ? e . message : String ( e ) ,
400429 }
401430 s . status [ clientName ] = failedStatus
402431 delete s . clients [ clientName ]
432+ return undefined
403433 } )
404- if ( ! tools ) {
434+ if ( ! toolsResult ) {
405435 continue
406436 }
407- for ( const [ toolName , tool ] of Object . entries ( tools ) ) {
437+ for ( const mcpTool of toolsResult . tools ) {
408438 const sanitizedClientName = clientName . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, "_" )
409- const sanitizedToolName = toolName . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, "_" )
410- result [ sanitizedClientName + "_" + sanitizedToolName ] = tool
439+ const sanitizedToolName = mcpTool . name . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, "_" )
440+ result [ sanitizedClientName + "_" + sanitizedToolName ] = convertMcpTool ( mcpTool , client )
411441 }
412442 }
413443 return result
@@ -469,10 +499,11 @@ export namespace MCP {
469499
470500 // Try to connect - this will trigger the OAuth flow
471501 try {
472- await experimental_createMCPClient ( {
502+ const client = new Client ( {
473503 name : "opencode" ,
474- transport ,
504+ version : Installation . VERSION ,
475505 } )
506+ await client . connect ( transport )
476507 // If we get here, we're already authenticated
477508 return { authorizationUrl : "" }
478509 } catch ( error ) {
0 commit comments