Model Context Protocol in OCaml
at tmp 17 kB view raw
1open Mcp 2open Mcp_sdk 3open Mcp_server 4 5(* Multimodal example MCP server *) 6 7(* Base64 encoding helper *) 8module Base64 = struct 9 let encode_char n = 10 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".[n] 11 12 let encode_block i bytes = 13 let buffer = Buffer.create 4 in 14 let b1 = Char.code (String.get bytes (i * 3)) in 15 let b2 = if i * 3 + 1 < String.length bytes then Char.code (String.get bytes (i * 3 + 1)) else 0 in 16 let b3 = if i * 3 + 2 < String.length bytes then Char.code (String.get bytes (i * 3 + 2)) else 0 in 17 18 let n = (b1 lsl 16) lor (b2 lsl 8) lor b3 in 19 Buffer.add_char buffer (encode_char ((n lsr 18) land 63)); 20 Buffer.add_char buffer (encode_char ((n lsr 12) land 63)); 21 22 if i * 3 + 1 < String.length bytes then 23 Buffer.add_char buffer (encode_char ((n lsr 6) land 63)) 24 else 25 Buffer.add_char buffer '='; 26 27 if i * 3 + 2 < String.length bytes then 28 Buffer.add_char buffer (encode_char (n land 63)) 29 else 30 Buffer.add_char buffer '='; 31 32 Buffer.contents buffer 33 34 let encode data = 35 let buffer = Buffer.create (4 * (String.length data + 2) / 3) in 36 for i = 0 to (String.length data - 1) / 3 do 37 Buffer.add_string buffer (encode_block i data) 38 done; 39 Buffer.contents buffer 40end 41 42(* Audio generator *) 43module AudioGenerator = struct 44 (* Generate a simple sine wave *) 45 let generate_sine_wave ~frequency ~duration ~sample_rate ~amplitude = 46 (* WAV parameters *) 47 let num_channels = 1 in (* Mono *) 48 let bits_per_sample = 16 in 49 let byte_rate = sample_rate * num_channels * bits_per_sample / 8 in 50 let block_align = num_channels * bits_per_sample / 8 in 51 let num_samples = int_of_float (float_of_int sample_rate *. duration) in 52 let data_size = num_samples * block_align in 53 54 (* Create buffer for the WAV data *) 55 let buffer = Buffer.create (44 + data_size) in 56 57 (* Write WAV header *) 58 (* "RIFF" chunk *) 59 Buffer.add_string buffer "RIFF"; 60 let file_size = 36 + data_size in 61 Buffer.add_char buffer (Char.chr (file_size land 0xff)); 62 Buffer.add_char buffer (Char.chr ((file_size lsr 8) land 0xff)); 63 Buffer.add_char buffer (Char.chr ((file_size lsr 16) land 0xff)); 64 Buffer.add_char buffer (Char.chr ((file_size lsr 24) land 0xff)); 65 Buffer.add_string buffer "WAVE"; 66 67 (* "fmt " sub-chunk *) 68 Buffer.add_string buffer "fmt "; 69 Buffer.add_char buffer (Char.chr 16); (* Sub-chunk size (16 for PCM) *) 70 Buffer.add_char buffer (Char.chr 0); 71 Buffer.add_char buffer (Char.chr 0); 72 Buffer.add_char buffer (Char.chr 0); 73 Buffer.add_char buffer (Char.chr 1); (* Audio format (1 for PCM) *) 74 Buffer.add_char buffer (Char.chr 0); 75 Buffer.add_char buffer (Char.chr num_channels); (* Number of channels *) 76 Buffer.add_char buffer (Char.chr 0); 77 78 (* Sample rate *) 79 Buffer.add_char buffer (Char.chr (sample_rate land 0xff)); 80 Buffer.add_char buffer (Char.chr ((sample_rate lsr 8) land 0xff)); 81 Buffer.add_char buffer (Char.chr ((sample_rate lsr 16) land 0xff)); 82 Buffer.add_char buffer (Char.chr ((sample_rate lsr 24) land 0xff)); 83 84 (* Byte rate *) 85 Buffer.add_char buffer (Char.chr (byte_rate land 0xff)); 86 Buffer.add_char buffer (Char.chr ((byte_rate lsr 8) land 0xff)); 87 Buffer.add_char buffer (Char.chr ((byte_rate lsr 16) land 0xff)); 88 Buffer.add_char buffer (Char.chr ((byte_rate lsr 24) land 0xff)); 89 90 (* Block align *) 91 Buffer.add_char buffer (Char.chr block_align); 92 Buffer.add_char buffer (Char.chr 0); 93 94 (* Bits per sample *) 95 Buffer.add_char buffer (Char.chr bits_per_sample); 96 Buffer.add_char buffer (Char.chr 0); 97 98 (* "data" sub-chunk *) 99 Buffer.add_string buffer "data"; 100 Buffer.add_char buffer (Char.chr (data_size land 0xff)); 101 Buffer.add_char buffer (Char.chr ((data_size lsr 8) land 0xff)); 102 Buffer.add_char buffer (Char.chr ((data_size lsr 16) land 0xff)); 103 Buffer.add_char buffer (Char.chr ((data_size lsr 24) land 0xff)); 104 105 (* Generate sine wave data *) 106 let max_amplitude = float_of_int (1 lsl (bits_per_sample - 1)) -. 1.0 in 107 for i = 0 to num_samples - 1 do 108 let t = float_of_int i /. float_of_int sample_rate in 109 let value = int_of_float (amplitude *. max_amplitude *. sin (2.0 *. Float.pi *. frequency *. t)) in 110 (* Write 16-bit sample (little-endian) *) 111 Buffer.add_char buffer (Char.chr (value land 0xff)); 112 Buffer.add_char buffer (Char.chr ((value lsr 8) land 0xff)); 113 done; 114 115 Buffer.contents buffer 116end 117 118(* Image generator *) 119module ImageGenerator = struct 120 (* Simple PNG generation *) 121 let generate_simple_image width height color_str = 122 (* Parse color - expected format: #RRGGBB or #RRGGBBAA *) 123 let r, g, b, a = 124 try 125 if String.length color_str >= 7 && color_str.[0] = '#' then 126 let r = int_of_string ("0x" ^ String.sub color_str 1 2) in 127 let g = int_of_string ("0x" ^ String.sub color_str 3 2) in 128 let b = int_of_string ("0x" ^ String.sub color_str 5 2) in 129 let a = if String.length color_str >= 9 then 130 int_of_string ("0x" ^ String.sub color_str 7 2) 131 else 255 in 132 (r, g, b, a) 133 else 134 (255, 0, 0, 255) (* Default to red if invalid *) 135 with _ -> 136 (255, 0, 0, 255) (* Default to red on parsing error *) 137 in 138 139 (* Create a very simple 1x1 PNG with the specified color *) 140 (* PNG signature *) 141 let signature = [137; 80; 78; 71; 13; 10; 26; 10] in 142 143 (* Create buffer for the PNG data *) 144 let buffer = Buffer.create 100 in 145 146 (* PNG signature *) 147 List.iter (fun b -> Buffer.add_char buffer (Char.chr b)) signature; 148 149 (* IHDR chunk *) 150 Buffer.add_char buffer (Char.chr 0); (* length - 13 bytes *) 151 Buffer.add_char buffer (Char.chr 0); 152 Buffer.add_char buffer (Char.chr 0); 153 Buffer.add_char buffer (Char.chr 13); 154 155 Buffer.add_string buffer "IHDR"; 156 157 (* Width *) 158 Buffer.add_char buffer (Char.chr ((width lsr 24) land 0xff)); 159 Buffer.add_char buffer (Char.chr ((width lsr 16) land 0xff)); 160 Buffer.add_char buffer (Char.chr ((width lsr 8) land 0xff)); 161 Buffer.add_char buffer (Char.chr (width land 0xff)); 162 163 (* Height *) 164 Buffer.add_char buffer (Char.chr ((height lsr 24) land 0xff)); 165 Buffer.add_char buffer (Char.chr ((height lsr 16) land 0xff)); 166 Buffer.add_char buffer (Char.chr ((height lsr 8) land 0xff)); 167 Buffer.add_char buffer (Char.chr (height land 0xff)); 168 169 Buffer.add_char buffer (Char.chr 8); (* Bit depth - 8 bits per channel *) 170 Buffer.add_char buffer (Char.chr 6); (* Color type - RGBA *) 171 Buffer.add_char buffer (Char.chr 0); (* Compression method - deflate *) 172 Buffer.add_char buffer (Char.chr 0); (* Filter method - adaptive filtering *) 173 Buffer.add_char buffer (Char.chr 0); (* Interlace method - no interlace *) 174 175 (* IHDR CRC - precomputed for simplicity *) 176 Buffer.add_char buffer (Char.chr 0); 177 Buffer.add_char buffer (Char.chr 0); 178 Buffer.add_char buffer (Char.chr 0); 179 Buffer.add_char buffer (Char.chr 0); 180 181 (* IDAT chunk - simplified for example *) 182 let pixels_per_row = width * 4 in 183 let data_size = (1 + pixels_per_row) * height in 184 185 Buffer.add_char buffer (Char.chr ((data_size lsr 24) land 0xff)); 186 Buffer.add_char buffer (Char.chr ((data_size lsr 16) land 0xff)); 187 Buffer.add_char buffer (Char.chr ((data_size lsr 8) land 0xff)); 188 Buffer.add_char buffer (Char.chr (data_size land 0xff)); 189 190 Buffer.add_string buffer "IDAT"; 191 192 (* Simple zlib header *) 193 Buffer.add_char buffer (Char.chr 0x78); 194 Buffer.add_char buffer (Char.chr 0x01); 195 196 (* Raw image data *) 197 for _ = 0 to height - 1 do 198 Buffer.add_char buffer (Char.chr 0); (* Filter type 0 - None *) 199 for _ = 0 to width - 1 do 200 Buffer.add_char buffer (Char.chr r); 201 Buffer.add_char buffer (Char.chr g); 202 Buffer.add_char buffer (Char.chr b); 203 Buffer.add_char buffer (Char.chr a); 204 done 205 done; 206 207 (* Dummy Adler32 checksum *) 208 Buffer.add_char buffer (Char.chr 0); 209 Buffer.add_char buffer (Char.chr 0); 210 Buffer.add_char buffer (Char.chr 0); 211 Buffer.add_char buffer (Char.chr 0); 212 213 (* IDAT CRC - precomputed for simplicity *) 214 Buffer.add_char buffer (Char.chr 0); 215 Buffer.add_char buffer (Char.chr 0); 216 Buffer.add_char buffer (Char.chr 0); 217 Buffer.add_char buffer (Char.chr 0); 218 219 (* IEND chunk *) 220 Buffer.add_char buffer (Char.chr 0); 221 Buffer.add_char buffer (Char.chr 0); 222 Buffer.add_char buffer (Char.chr 0); 223 Buffer.add_char buffer (Char.chr 0); 224 225 Buffer.add_string buffer "IEND"; 226 227 (* IEND CRC - precomputed value *) 228 Buffer.add_char buffer (Char.chr 0xAE); 229 Buffer.add_char buffer (Char.chr 0x42); 230 Buffer.add_char buffer (Char.chr 0x60); 231 Buffer.add_char buffer (Char.chr 0x82); 232 233 Buffer.contents buffer 234end 235 236(* Helper for extracting values from JSON *) 237let get_param_int json name default = 238 match json with 239 | `Assoc fields -> begin 240 match List.assoc_opt name fields with 241 | Some (`Int i) -> begin 242 i 243 end 244 | Some (`Float f) -> begin 245 int_of_float f 246 end 247 | _ -> begin 248 default 249 end 250 end 251 | _ -> begin 252 default 253 end 254 255let get_param_float json name default = 256 match json with 257 | `Assoc fields -> begin 258 match List.assoc_opt name fields with 259 | Some (`Int i) -> begin 260 float_of_int i 261 end 262 | Some (`Float f) -> begin 263 f 264 end 265 | _ -> begin 266 default 267 end 268 end 269 | _ -> begin 270 default 271 end 272 273let get_param_string json name default = 274 match json with 275 | `Assoc fields -> begin 276 match List.assoc_opt name fields with 277 | Some (`String s) -> begin 278 s 279 end 280 | _ -> begin 281 default 282 end 283 end 284 | _ -> begin 285 default 286 end 287 288(* Create a server *) 289let server = create_server 290 ~name:"OCaml MCP Multimodal Example" 291 ~version:"0.1.0" 292 ~protocol_version:"2024-11-05" 293 () 294 295(* Define startup and shutdown hooks *) 296let startup () = 297 Printf.fprintf stderr "MultimodalServer is starting up!\n"; 298 flush stderr; 299 Log.info "MultimodalServer is starting up!" 300 301let shutdown () = 302 Printf.fprintf stderr "MultimodalServer is shutting down. Goodbye!\n"; 303 flush stderr; 304 Log.info "MultimodalServer is shutting down. Goodbye!" 305 306(* Register the hooks *) 307let () = 308 set_startup_hook server startup; 309 set_shutdown_hook server shutdown 310 311(* Define and register a multimodal tool *) 312let _ = add_tool server 313 ~name:"generate_multimodal_response" 314 ~description:"Generates a response with text, image and audio content" 315 ~schema_properties:[ 316 ("message", "string", "The text message to include"); 317 ("color", "string", "Color for the image (hex format #RRGGBB)"); 318 ("frequency", "integer", "Frequency for the audio tone in Hz"); 319 ] 320 ~schema_required:["message"] 321 (fun args -> 322 try 323 let message = get_param_string args "message" "Hello, multimodal world!" in 324 let color = get_param_string args "color" "#FF0000" in 325 let frequency = get_param_int args "frequency" 440 in 326 327 (* Generate image *) 328 let image_data = ImageGenerator.generate_simple_image 100 100 color in 329 let image_base64 = Base64.encode image_data in 330 331 (* Generate audio *) 332 let audio_data = AudioGenerator.generate_sine_wave 333 ~frequency:(float_of_int frequency) 334 ~duration:1.0 335 ~sample_rate:8000 336 ~amplitude:0.8 in 337 let audio_base64 = Base64.encode audio_data in 338 339 (* Create a response with text, image and audio content *) 340 CallToolResult.yojson_of_t CallToolResult.{ 341 content = [ 342 Text TextContent.{ 343 text = message; 344 annotations = None 345 }; 346 Image ImageContent.{ 347 data = image_base64; 348 mime_type = "image/png"; 349 annotations = None 350 }; 351 Audio AudioContent.{ 352 data = audio_base64; 353 mime_type = "audio/wav"; 354 annotations = None 355 } 356 ]; 357 is_error = false; 358 meta = None 359 } 360 with 361 | Failure msg -> 362 Log.error (Printf.sprintf "Error in multimodal tool: %s" msg); 363 CallToolResult.yojson_of_t CallToolResult.{ 364 content = [ 365 Text TextContent.{ 366 text = Printf.sprintf "Error: %s" msg; 367 annotations = None 368 } 369 ]; 370 is_error = true; 371 meta = None 372 } 373 ) 374 375(* Define and register a multimodal prompt *) 376let _ = add_prompt server 377 ~name:"multimodal-prompt" 378 ~description:"A prompt that includes text, image, and audio" 379 ~arguments:[ 380 ("message", Some "Text message to include", true); 381 ("color", Some "Color for the image (hex format #RRGGBB)", false); 382 ("frequency", Some "Frequency for the audio tone in Hz", false); 383 ] 384 (fun args -> 385 (* Parse parameters with defaults *) 386 let message = 387 try List.assoc "message" args 388 with Not_found -> "Hello, multimodal world!" 389 in 390 391 let color = 392 try List.assoc "color" args 393 with Not_found -> "#0000FF" 394 in 395 396 let frequency = 397 try int_of_string (List.assoc "frequency" args) 398 with _ -> 440 399 in 400 401 (* Generate image *) 402 let image_data = ImageGenerator.generate_simple_image 100 100 color in 403 let image_base64 = Base64.encode image_data in 404 405 (* Generate audio *) 406 let audio_data = AudioGenerator.generate_sine_wave 407 ~frequency:(float_of_int frequency) 408 ~duration:1.0 409 ~sample_rate:8000 410 ~amplitude:0.8 in 411 let audio_base64 = Base64.encode audio_data in 412 413 (* Create a multimodal prompt *) 414 [ 415 Prompt.{ 416 role = `User; 417 content = make_text_content "Here's a multimodal message with text, image, and audio:" 418 }; 419 Prompt.{ 420 role = `User; 421 content = make_text_content message 422 }; 423 Prompt.{ 424 role = `User; 425 content = make_image_content image_base64 "image/png" 426 }; 427 Prompt.{ 428 role = `User; 429 content = make_audio_content audio_base64 "audio/wav" 430 }; 431 Prompt.{ 432 role = `Assistant; 433 content = make_text_content "I've received your multimodal message with text, image, and audio." 434 } 435 ] 436 ) 437 438(* Also register a resource prompt example *) 439let _ = add_prompt server 440 ~name:"resource-prompt" 441 ~description:"A prompt that includes embedded resources" 442 ~arguments:[ 443 ("resource_id", Some "ID of the resource to include", true); 444 ] 445 (fun args -> 446 (* Sample resource texts *) 447 let resources = [ 448 ("doc1", "This is the content of document 1."); 449 ("doc2", "Document 2 contains important information about OCaml."); 450 ("doc3", "Document 3 explains the MCP protocol in detail."); 451 ] in 452 453 (* Get the requested resource *) 454 let resource_id = 455 try List.assoc "resource_id" args 456 with Not_found -> "doc1" 457 in 458 459 (* Find the resource content *) 460 let resource_content = 461 try List.assoc resource_id resources 462 with Not_found -> Printf.sprintf "Resource '%s' not found" resource_id 463 in 464 465 (* Create a prompt with embedded resource *) 466 [ 467 Prompt.{ 468 role = `User; 469 content = make_text_content (Printf.sprintf "Here's the content of resource %s:" resource_id) 470 }; 471 Prompt.{ 472 role = `User; 473 content = make_text_resource_content (Printf.sprintf "resource://%s" resource_id) resource_content ~mime_type:"text/plain" () 474 }; 475 Prompt.{ 476 role = `User; 477 content = make_text_content "Please analyze this content." 478 }; 479 Prompt.{ 480 role = `Assistant; 481 content = make_text_content "I'll analyze the resource content for you." 482 } 483 ] 484 ) 485 486(* Main function *) 487let () = 488 (* Parse command line arguments *) 489 let transport_type = ref Stdio in 490 let args = [ 491 ("--http", Arg.Unit (fun () -> transport_type := Http), 492 "Start server with HTTP transport (default is stdio)"); 493 ] in 494 let usage_msg = "Usage: multimodal_example [--http]" in 495 Arg.parse args (fun _ -> ()) usage_msg; 496 497 (* Configure the server with appropriate capabilities *) 498 let server = configure_server server ~with_tools:true ~with_resources:false ~with_prompts:true () in 499 500 (* Create and start MCP server with the selected transport *) 501 let mcp_server = create ~server ~transport:!transport_type () in 502 start mcp_server;