Model Context Protocol in OCaml
1open Mcp_sdk 2 3(* Helper for extracting string value from JSON *) 4let get_string_param json name = 5 match json with 6 | `Assoc fields -> 7 (match List.assoc_opt name fields with 8 | Some (`String value) -> value 9 | _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name))) 10 | _ -> raise (Failure "Expected JSON object") 11 12(* Helper for extracting integer value from JSON *) 13let get_int_param json name = 14 match json with 15 | `Assoc fields -> 16 (match List.assoc_opt name fields with 17 | Some (`Int value) -> value 18 | Some (`String value) -> int_of_string value 19 | _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name))) 20 | _ -> raise (Failure "Expected JSON object") 21 22(* Base64 encoding - simplified version *) 23module Base64 = struct 24 let encode_char idx = 25 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".[idx] 26 27 let encode s = 28 let len = String.length s in 29 let result = Bytes.create (((len + 2) / 3) * 4) in 30 31 let rec loop i j = 32 if i >= len then j 33 else 34 let n = 35 let n = Char.code s.[i] lsl 16 in 36 let n = if i + 1 < len then n lor (Char.code s.[i+1] lsl 8) else n in 37 if i + 2 < len then n lor Char.code s.[i+2] else n 38 in 39 Bytes.set result j (encode_char ((n lsr 18) land 63)); 40 Bytes.set result (j+1) (encode_char ((n lsr 12) land 63)); 41 Bytes.set result (j+2) 42 (if i + 1 < len then encode_char ((n lsr 6) land 63) else '='); 43 Bytes.set result (j+3) 44 (if i + 2 < len then encode_char (n land 63) else '='); 45 loop (i + 3) (j + 4) 46 in 47 Bytes.sub_string result 0 (loop 0 0) 48end 49 50(* Generate a simple GIF format image *) 51let generate_random_image width height = 52 (* Ensure dimensions are reasonable *) 53 let width = min 256 (max 16 width) in 54 let height = min 256 (max 16 height) in 55 56 (* Create a buffer for GIF data *) 57 let buf = Buffer.create 1024 in 58 59 (* GIF Header - "GIF89a" *) 60 Buffer.add_string buf "GIF89a"; 61 62 (* Logical Screen Descriptor *) 63 (* Width - 2 bytes little endian *) 64 Buffer.add_char buf (Char.chr (width land 0xff)); 65 Buffer.add_char buf (Char.chr ((width lsr 8) land 0xff)); 66 67 (* Height - 2 bytes little endian *) 68 Buffer.add_char buf (Char.chr (height land 0xff)); 69 Buffer.add_char buf (Char.chr ((height lsr 8) land 0xff)); 70 71 (* Packed fields - 1 byte: 72 Global Color Table Flag - 1 bit (1) 73 Color Resolution - 3 bits (7 = 8 bits per color) 74 Sort Flag - 1 bit (0) 75 Size of Global Color Table - 3 bits (2 = 8 colors) *) 76 Buffer.add_char buf (Char.chr 0xF2); 77 78 (* Background color index - 1 byte *) 79 Buffer.add_char buf (Char.chr 0); 80 81 (* Pixel aspect ratio - 1 byte *) 82 Buffer.add_char buf (Char.chr 0); 83 84 (* Global Color Table - 8 colors x 3 bytes (R,G,B) *) 85 (* Simple 8-color palette *) 86 Buffer.add_string buf "\xFF\xFF\xFF"; (* White (0) *) 87 Buffer.add_string buf "\xFF\x00\x00"; (* Red (1) *) 88 Buffer.add_string buf "\x00\xFF\x00"; (* Green (2) *) 89 Buffer.add_string buf "\x00\x00\xFF"; (* Blue (3) *) 90 Buffer.add_string buf "\xFF\xFF\x00"; (* Yellow (4) *) 91 Buffer.add_string buf "\xFF\x00\xFF"; (* Magenta (5) *) 92 Buffer.add_string buf "\x00\xFF\xFF"; (* Cyan (6) *) 93 Buffer.add_string buf "\x00\x00\x00"; (* Black (7) *) 94 95 (* Graphics Control Extension (optional) *) 96 Buffer.add_char buf (Char.chr 0x21); (* Extension Introducer *) 97 Buffer.add_char buf (Char.chr 0xF9); (* Graphic Control Label *) 98 Buffer.add_char buf (Char.chr 0x04); (* Block Size *) 99 Buffer.add_char buf (Char.chr 0x01); (* Packed field: 1 bit for transparency *) 100 Buffer.add_char buf (Char.chr 0x00); (* Delay time (1/100s) - 2 bytes *) 101 Buffer.add_char buf (Char.chr 0x00); 102 Buffer.add_char buf (Char.chr 0x00); (* Transparent color index *) 103 Buffer.add_char buf (Char.chr 0x00); (* Block terminator *) 104 105 (* Image Descriptor *) 106 Buffer.add_char buf (Char.chr 0x2C); (* Image Separator *) 107 Buffer.add_char buf (Char.chr 0x00); (* Left position - 2 bytes *) 108 Buffer.add_char buf (Char.chr 0x00); 109 Buffer.add_char buf (Char.chr 0x00); (* Top position - 2 bytes *) 110 Buffer.add_char buf (Char.chr 0x00); 111 112 (* Image width - 2 bytes little endian *) 113 Buffer.add_char buf (Char.chr (width land 0xff)); 114 Buffer.add_char buf (Char.chr ((width lsr 8) land 0xff)); 115 116 (* Image height - 2 bytes little endian *) 117 Buffer.add_char buf (Char.chr (height land 0xff)); 118 Buffer.add_char buf (Char.chr ((height lsr 8) land 0xff)); 119 120 (* Packed fields - 1 byte - no local color table *) 121 Buffer.add_char buf (Char.chr 0x00); 122 123 (* LZW Minimum Code Size - 1 byte *) 124 Buffer.add_char buf (Char.chr 0x03); (* Minimum code size 3 for 8 colors *) 125 126 (* Generate a simple image - a checkerboard pattern *) 127 let step = width / 8 in 128 let image_data = Buffer.create (width * height / 4) in 129 130 (* Very simple LZW compression - just store raw clear codes and color indexes *) 131 (* Start with Clear code *) 132 Buffer.add_char image_data (Char.chr 0x08); (* Clear code 8 *) 133 134 (* For very simple encoding, we'll just use a sequence of color indexes *) 135 for y = 0 to height - 1 do 136 for x = 0 to width - 1 do 137 (* Checkerboard pattern with different colors *) 138 let color = 139 if ((x / step) + (y / step)) mod 2 = 0 then 140 3 (* Blue *) 141 else 142 1 (* Red *) 143 in 144 Buffer.add_char image_data (Char.chr color); 145 done 146 done; 147 148 (* End with End of Information code *) 149 Buffer.add_char image_data (Char.chr 0x09); 150 151 (* Add image data blocks - GIF uses 255-byte max chunks *) 152 let data = Buffer.contents image_data in 153 let data_len = String.length data in 154 let pos = ref 0 in 155 156 while !pos < data_len do 157 let chunk_size = min 255 (data_len - !pos) in 158 Buffer.add_char buf (Char.chr chunk_size); 159 for i = 0 to chunk_size - 1 do 160 Buffer.add_char buf (String.get data (!pos + i)); 161 done; 162 pos := !pos + chunk_size; 163 done; 164 165 (* Zero-length block to end the image data *) 166 Buffer.add_char buf (Char.chr 0x00); 167 168 (* GIF Trailer *) 169 Buffer.add_char buf (Char.chr 0x3B); 170 171 (* Base64 encode the GIF data *) 172 Base64.encode (Buffer.contents buf) 173 174(* Helper to write 32-bit little endian integer *) 175let write_int32_le buf n = 176 Buffer.add_char buf (Char.chr (n land 0xff)); 177 Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff)); 178 Buffer.add_char buf (Char.chr ((n lsr 16) land 0xff)); 179 Buffer.add_char buf (Char.chr ((n lsr 24) land 0xff)) 180 181(* Helper to write 16-bit little endian integer *) 182let write_int16_le buf n = 183 Buffer.add_char buf (Char.chr (n land 0xff)); 184 Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff)) 185 186(* Generate a simple WAV file with sine wave *) 187let generate_sine_wave_audio frequency duration = 188 (* WAV header *) 189 let sample_rate = 8000 in 190 let num_samples = sample_rate * duration in 191 let header_buf = Buffer.create 44 in 192 193 (* Fill WAV header properly *) 194 Buffer.add_string header_buf "RIFF"; 195 write_int32_le header_buf (36 + num_samples * 2); (* File size minus 8 *) 196 Buffer.add_string header_buf "WAVE"; 197 198 (* Format chunk *) 199 Buffer.add_string header_buf "fmt "; 200 write_int32_le header_buf 16; (* Format chunk size *) 201 write_int16_le header_buf 1; (* PCM format *) 202 write_int16_le header_buf 1; (* Mono *) 203 write_int32_le header_buf sample_rate; (* Sample rate *) 204 write_int32_le header_buf (sample_rate * 2); (* Byte rate *) 205 write_int16_le header_buf 2; (* Block align *) 206 write_int16_le header_buf 16; (* Bits per sample *) 207 208 (* Data chunk *) 209 Buffer.add_string header_buf "data"; 210 write_int32_le header_buf (num_samples * 2); (* Data size *) 211 212 (* Generate sine wave samples *) 213 let samples_buf = Buffer.create (num_samples * 2) in 214 let amplitude = 16384.0 in (* 16-bit with headroom *) 215 216 for i = 0 to num_samples - 1 do 217 let t = float_of_int i /. float_of_int sample_rate in 218 let value = amplitude *. sin (2.0 *. Float.pi *. frequency *. t) in 219 let sample = int_of_float value in 220 221 (* Convert to 16-bit little-endian *) 222 let sample = if sample < 0 then sample + 65536 else sample in 223 write_int16_le samples_buf sample; 224 done; 225 226 (* Combine header and samples, then encode as Base64 *) 227 let wav_data = Buffer.contents header_buf ^ Buffer.contents samples_buf in 228 Base64.encode wav_data 229 230(* Create a server *) 231let server = create_server 232 ~name:"OCaml MCP Multimodal Example" 233 ~version:"0.1.0" 234 ~protocol_version:"2024-11-05" () |> 235 fun server -> 236 (* Set default capabilities *) 237 configure_server server ~with_tools:true ~with_resources:true ~with_prompts:true () 238 239(* Define and register a multimodal tool that returns text, images, and audio *) 240let _ = add_tool server 241 ~name:"multimodal_demo" 242 ~description:"Demonstrates multimodal content with text, image, and audio" 243 ~schema_properties:[ 244 ("width", "integer", "Width of the generated image (pixels)"); 245 ("height", "integer", "Height of the generated image (pixels)"); 246 ("frequency", "integer", "Frequency of the generated audio tone (Hz)"); 247 ("duration", "integer", "Duration of the generated audio (seconds)"); 248 ("message", "string", "Text message to include") 249 ] 250 ~schema_required:["message"] 251 (fun args -> 252 try 253 (* Extract parameters with defaults if not provided *) 254 let message = get_string_param args "message" in 255 let width = try get_int_param args "width" with _ -> 128 in 256 let height = try get_int_param args "height" with _ -> 128 in 257 let frequency = try get_int_param args "frequency" with _ -> 440 in 258 let duration = try get_int_param args "duration" with _ -> 1 in 259 260 (* Generate image and audio data *) 261 let image_data = generate_random_image width height in 262 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in 263 264 (* Create a multimodal tool result *) 265 Tool.create_tool_result [ 266 Mcp.make_text_content message; 267 Mcp.make_image_content image_data "image/gif"; 268 Mcp.make_audio_content audio_data "audio/wav" 269 ] ~is_error:false 270 with 271 | Failure msg -> 272 Log.error (Printf.sprintf "Error in multimodal tool: %s" msg); 273 Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true 274 ) 275 276(* Define and register a tool for generating only images *) 277let _ = add_tool server 278 ~name:"generate_image" 279 ~description:"Generates a random image with specified dimensions" 280 ~schema_properties:[ 281 ("width", "integer", "Width of the generated image (pixels)"); 282 ("height", "integer", "Height of the generated image (pixels)") 283 ] 284 ~schema_required:["width"; "height"] 285 (fun args -> 286 try 287 let width = get_int_param args "width" in 288 let height = get_int_param args "height" in 289 290 if width < 1 || width > 1024 || height < 1 || height > 1024 then 291 Tool.create_tool_result 292 [Mcp.make_text_content "Error: Dimensions must be between 1 and 1024 pixels"] 293 ~is_error:true 294 else 295 let image_data = generate_random_image width height in 296 Tool.create_tool_result 297 [Mcp.make_image_content image_data "image/gif"] 298 ~is_error:false 299 with 300 | Failure msg -> 301 Log.error (Printf.sprintf "Error in generate_image tool: %s" msg); 302 Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true 303 ) 304 305(* Define and register a tool for generating only audio *) 306let _ = add_tool server 307 ~name:"generate_audio" 308 ~description:"Generates an audio tone with specified frequency and duration" 309 ~schema_properties:[ 310 ("frequency", "integer", "Frequency of the tone in Hz (20-20000)"); 311 ("duration", "integer", "Duration of the tone in seconds (1-10)") 312 ] 313 ~schema_required:["frequency"; "duration"] 314 (fun args -> 315 try 316 let frequency = get_int_param args "frequency" in 317 let duration = get_int_param args "duration" in 318 319 if frequency < 20 || frequency > 20000 then 320 Tool.create_tool_result 321 [Mcp.make_text_content "Error: Frequency must be between 20Hz and 20,000Hz"] 322 ~is_error:true 323 else if duration < 1 || duration > 10 then 324 Tool.create_tool_result 325 [Mcp.make_text_content "Error: Duration must be between 1 and 10 seconds"] 326 ~is_error:true 327 else 328 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in 329 Tool.create_tool_result 330 [Mcp.make_audio_content audio_data "audio/wav"] 331 ~is_error:false 332 with 333 | Failure msg -> 334 Log.error (Printf.sprintf "Error in generate_audio tool: %s" msg); 335 Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true 336 ) 337 338(* Define and register a resource example with multimodal content *) 339let _ = add_resource server 340 ~uri_template:"multimodal://{name}" 341 ~description:"Get a multimodal greeting with text, image and audio" 342 ~mime_type:"application/json" 343 (fun params -> 344 match params with 345 | [name] -> 346 let greeting = Printf.sprintf "Hello, %s! Welcome to the multimodal MCP example." name in 347 let image_data = generate_random_image 128 128 in 348 let audio_data = generate_sine_wave_audio 440.0 1 in 349 350 Printf.sprintf {| 351 { 352 "greeting": "%s", 353 "image": { 354 "data": "%s", 355 "mimeType": "image/gif" 356 }, 357 "audio": { 358 "data": "%s", 359 "mimeType": "audio/wav" 360 } 361 } 362 |} greeting image_data audio_data 363 | _ -> Printf.sprintf {|{"error": "Invalid parameters"}|} 364 ) 365 366(* Run the server with the default scheduler *) 367let () = 368 Random.self_init(); (* Initialize random generator *) 369 Eio_main.run @@ fun env -> 370 Mcp_server.run_server env server