@@ -9,6 +9,105 @@ import { createRuntime } from "@/node/runtime/runtimeFactory";
99
1010const TEST_TIMEOUT_MS = 10_000 ;
1111
12+ /**
13+ * MCP CallToolResult content types (from @ai-sdk/mcp)
14+ */
15+ interface MCPTextContent {
16+ type : "text" ;
17+ text : string ;
18+ }
19+
20+ interface MCPImageContent {
21+ type : "image" ;
22+ data : string ; // base64
23+ mimeType : string ;
24+ }
25+
26+ interface MCPResourceContent {
27+ type : "resource" ;
28+ resource : { uri : string ; text ?: string ; blob ?: string ; mimeType ?: string } ;
29+ }
30+
31+ type MCPContent = MCPTextContent | MCPImageContent | MCPResourceContent ;
32+
33+ interface MCPCallToolResult {
34+ content ?: MCPContent [ ] ;
35+ isError ?: boolean ;
36+ toolResult ?: unknown ;
37+ }
38+
39+ /**
40+ * AI SDK LanguageModelV2ToolResultOutput content types
41+ */
42+ type AISDKContentPart =
43+ | { type : "text" ; text : string }
44+ | { type : "media" ; data : string ; mediaType : string } ;
45+
46+ /**
47+ * Transform MCP tool result to AI SDK format.
48+ * Converts MCP's "image" content type to AI SDK's "media" type.
49+ */
50+ function transformMCPResult ( result : MCPCallToolResult ) : unknown {
51+ // If it's an error or has toolResult, pass through as-is
52+ if ( result . isError || result . toolResult !== undefined ) {
53+ return result ;
54+ }
55+
56+ // If no content array, pass through
57+ if ( ! result . content || ! Array . isArray ( result . content ) ) {
58+ return result ;
59+ }
60+
61+ // Check if any content is an image
62+ const hasImage = result . content . some ( ( c ) => c . type === "image" ) ;
63+ if ( ! hasImage ) {
64+ return result ;
65+ }
66+
67+ // Transform to AI SDK content format
68+ const transformedContent : AISDKContentPart [ ] = result . content . map ( ( item ) => {
69+ if ( item . type === "text" ) {
70+ return { type : "text" as const , text : item . text } ;
71+ }
72+ if ( item . type === "image" ) {
73+ return { type : "media" as const , data : item . data , mediaType : item . mimeType } ;
74+ }
75+ // For resource type, convert to text representation
76+ if ( item . type === "resource" ) {
77+ const text = item . resource . text ?? item . resource . uri ;
78+ return { type : "text" as const , text } ;
79+ }
80+ // Fallback: stringify unknown content
81+ return { type : "text" as const , text : JSON . stringify ( item ) } ;
82+ } ) ;
83+
84+ return { type : "content" , value : transformedContent } ;
85+ }
86+
87+ /**
88+ * Wrap MCP tools to transform their results to AI SDK format.
89+ * This ensures image content is properly converted to media type.
90+ */
91+ function wrapMCPTools ( tools : Record < string , Tool > ) : Record < string , Tool > {
92+ const wrapped : Record < string , Tool > = { } ;
93+ for ( const [ name , tool ] of Object . entries ( tools ) ) {
94+ // Only wrap tools that have an execute function
95+ if ( ! tool . execute ) {
96+ wrapped [ name ] = tool ;
97+ continue ;
98+ }
99+ const originalExecute = tool . execute ;
100+ wrapped [ name ] = {
101+ ...tool ,
102+ execute : async ( args : Parameters < typeof originalExecute > [ 0 ] , options ) => {
103+ const result : unknown = await originalExecute ( args , options ) ;
104+ return transformMCPResult ( result as MCPCallToolResult ) ;
105+ } ,
106+ } ;
107+ }
108+ return wrapped ;
109+ }
110+
12111export type MCPTestResult = { success : true ; tools : string [ ] } | { success : false ; error : string } ;
13112
14113interface MCPServerInstance {
@@ -231,7 +330,8 @@ export class MCPServerManager {
231330
232331 await transport . start ( ) ;
233332 const client = await experimental_createMCPClient ( { transport } ) ;
234- const tools = await client . tools ( ) ;
333+ const rawTools = await client . tools ( ) ;
334+ const tools = wrapMCPTools ( rawTools ) ;
235335 const toolNames = Object . keys ( tools ) ;
236336 log . info ( "[MCP] Server ready" , { name, tools : toolNames } ) ;
237337
0 commit comments