11import type { Message } from "@codeworksh/aikit" ;
22import type { CallToolResult , ContentBlock } from "@modelcontextprotocol/client" ;
33import { Effect } from "effect" ;
4+ import { Image } from "../tool/image/index.ts" ;
45import { formatSize , truncateMiddle } from "../tool/truncate.ts" ;
56
67export type Content = Message . TextContent | Message . ImageContent ;
@@ -33,56 +34,76 @@ const extensionOf = (uri: string) => {
3334 return / \. [ A - Z a - z 0 - 9 ] { 1 , 8 } $ / . exec ( path ) ?. [ 0 ] ?? ".bin" ;
3435} ;
3536
37+ /**
38+ * An image as the model can take it inline, as the read tool does: in a supported format and within
39+ * the provider's limits, or a note saying why it was left out.
40+ */
41+ const image = ( data : string , mimeType : string ) =>
42+ Image . inline ( Buffer . from ( data , "base64" ) , mimeType ) . pipe (
43+ Effect . map ( ( inline ) : ReadonlyArray < Content > =>
44+ inline . ok
45+ ? [
46+ { type : "image" , data : inline . data , mimeType : inline . mimeType } ,
47+ ...inline . hints . map ( ( text ) : Content => ( { type : "text" , text } ) ) ,
48+ ]
49+ : [ { type : "text" , text : inline . message } ] ,
50+ ) ,
51+ ) ;
52+
3653/** Blocks that need neither a file nor the server's name. */
37- const plain = ( block : ContentBlock ) : Content => {
54+ const plain = ( block : ContentBlock ) : Effect . Effect < ReadonlyArray < Content > > => {
3855 switch ( block . type ) {
3956 case "text" :
40- return { type : "text" , text : block . text } ;
57+ return Effect . succeed ( [ { type : "text" , text : block . text } ] ) ;
4158 case "image" :
42- return { type : " image" , data : block . data , mimeType : block . mimeType } ;
59+ return image ( block . data , block . mimeType ) ;
4360 case "audio" :
44- return { type : "text" , text : `[audio ${ block . mimeType } omitted]` } ;
61+ return Effect . succeed ( [ { type : "text" , text : `[audio ${ block . mimeType } omitted]` } ] ) ;
4562 case "resource" : {
4663 const resource = block . resource ;
4764 return "text" in resource
48- ? { type : "text" , text : resource . text }
49- : { type : " image" , data : resource . blob , mimeType : resource . mimeType ?? "image/png" } ;
65+ ? Effect . succeed ( [ { type : "text" , text : resource . text } ] )
66+ : image ( resource . blob , resource . mimeType ?? "image/png" ) ;
5067 }
5168 default :
52- return { type : "text" , text : `[unsupported MCP content ${ block . type } ]` } ;
69+ return Effect . succeed ( [ { type : "text" , text : `[unsupported MCP content ${ block . type } ]` } ] ) ;
5370 }
5471} ;
5572
5673/** One block of `server`'s output as model content. */
57- const fromBlock = Effect . fnUntraced ( function * ( server : string , block : ContentBlock , options : Options ) {
74+ const fromBlock = Effect . fnUntraced ( function * (
75+ server : string ,
76+ block : ContentBlock ,
77+ options : Options ,
78+ ) : Effect . fn . Return < ReadonlyArray < Content > > {
5879 if ( block . type === "resource_link" ) {
5980 const details = [ block . mimeType , block . size === undefined ? undefined : formatSize ( block . size ) ] . filter ( Boolean ) ;
6081 const read = options . readable ? `. Read it with ${ READ_RESOURCE_TOOL } (server "${ server } ")` : "" ;
6182 const description = block . description ? `: ${ block . description } ` : "" ;
62- return {
63- type : "text" ,
64- text : `[Resource ${ block . uri } "${ block . title ?? block . name } "${ details . length > 0 ? ` (${ details . join ( ", " ) } )` : "" } ${ description } ${ read } ]` ,
65- } satisfies Content ;
83+ return [
84+ {
85+ type : "text" ,
86+ text : `[Resource ${ block . uri } "${ block . title ?? block . name } "${ details . length > 0 ? ` (${ details . join ( ", " ) } )` : "" } ${ description } ${ read } ]` ,
87+ } ,
88+ ] ;
6689 }
6790 if ( block . type === "resource" && "blob" in block . resource && ! block . resource . mimeType ?. startsWith ( "image/" ) ) {
6891 const { uri, mimeType, blob } = block . resource ;
6992 const data = Buffer . from ( blob , "base64" ) ;
70- if ( isText ( mimeType ) ) return { type : "text" , text : data . toString ( "utf8" ) } satisfies Content ;
93+ if ( isText ( mimeType ) ) return [ { type : "text" , text : data . toString ( "utf8" ) } ] ;
7194 const kind = `${ mimeType ?? "unknown type" } , ${ formatSize ( data . length ) } ` ;
7295 return yield * options . save ( data , extensionOf ( uri ) ) . pipe (
7396 Effect . match ( {
74- onSuccess : ( path ) : Content => ( {
75- type : "text" ,
76- text : `[Binary resource ${ uri } (${ kind } ) saved to ${ path } ]` ,
77- } ) ,
78- onFailure : ( reason ) : Content => ( {
79- type : "text" ,
80- text : `[Binary resource ${ uri } (${ kind } ) could not be saved: ${ reason } ]` ,
81- } ) ,
97+ onSuccess : ( path ) : ReadonlyArray < Content > => [
98+ { type : "text" , text : `[Binary resource ${ uri } (${ kind } ) saved to ${ path } ]` } ,
99+ ] ,
100+ onFailure : ( reason ) : ReadonlyArray < Content > => [
101+ { type : "text" , text : `[Binary resource ${ uri } (${ kind } ) could not be saved: ${ reason } ]` } ,
102+ ] ,
82103 } ) ,
83104 ) ;
84105 }
85- return plain ( block ) ;
106+ return yield * plain ( block ) ;
86107} ) ;
87108
88109/**
@@ -106,7 +127,7 @@ export const limit = Effect.fnUntraced(function* (content: ReadonlyArray<Content
106127
107128/** Blocks of `server`'s output as model content, before {@link limit}. */
108129export const fromBlocks = ( server : string , blocks : ReadonlyArray < ContentBlock > , options : Options ) =>
109- Effect . forEach ( blocks , ( block ) => fromBlock ( server , block , options ) ) ;
130+ Effect . forEach ( blocks , ( block ) => fromBlock ( server , block , options ) ) . pipe ( Effect . map ( ( all ) => all . flat ( ) ) ) ;
110131
111132/**
112133 * A tool result as model content. A result without content blocks but with `structuredContent`
0 commit comments