Model Context Protocol in OCaml
at tmp 12 kB view raw
1open Mcp 2open Mcp_sdk 3open Mcp_server 4 5(* WAV file format helper module *) 6module Wav = struct 7 (* Simple WAV file generation for a sine wave *) 8 let generate_sine_wave ~frequency ~duration ~sample_rate ~amplitude = 9 (* WAV parameters *) 10 let num_channels = 1 in (* Mono *) 11 let bits_per_sample = 16 in 12 let byte_rate = sample_rate * num_channels * bits_per_sample / 8 in 13 let block_align = num_channels * bits_per_sample / 8 in 14 let num_samples = int_of_float (float_of_int sample_rate *. duration) in 15 let data_size = num_samples * block_align in 16 17 (* Create buffer for the WAV data *) 18 let buffer = Buffer.create (44 + data_size) in 19 20 (* Write WAV header *) 21 (* "RIFF" chunk *) 22 Buffer.add_string buffer "RIFF"; 23 let file_size = 36 + data_size in 24 Buffer.add_char buffer (char_of_int (file_size land 0xff)); 25 Buffer.add_char buffer (char_of_int ((file_size lsr 8) land 0xff)); 26 Buffer.add_char buffer (char_of_int ((file_size lsr 16) land 0xff)); 27 Buffer.add_char buffer (char_of_int ((file_size lsr 24) land 0xff)); 28 Buffer.add_string buffer "WAVE"; 29 30 (* "fmt " sub-chunk *) 31 Buffer.add_string buffer "fmt "; 32 Buffer.add_char buffer (char_of_int 16); (* Sub-chunk size (16 for PCM) *) 33 Buffer.add_char buffer (char_of_int 0); 34 Buffer.add_char buffer (char_of_int 0); 35 Buffer.add_char buffer (char_of_int 0); 36 Buffer.add_char buffer (char_of_int 1); (* Audio format (1 for PCM) *) 37 Buffer.add_char buffer (char_of_int 0); 38 Buffer.add_char buffer (char_of_int num_channels); (* Number of channels *) 39 Buffer.add_char buffer (char_of_int 0); 40 41 (* Sample rate *) 42 Buffer.add_char buffer (char_of_int (sample_rate land 0xff)); 43 Buffer.add_char buffer (char_of_int ((sample_rate lsr 8) land 0xff)); 44 Buffer.add_char buffer (char_of_int ((sample_rate lsr 16) land 0xff)); 45 Buffer.add_char buffer (char_of_int ((sample_rate lsr 24) land 0xff)); 46 47 (* Byte rate *) 48 Buffer.add_char buffer (char_of_int (byte_rate land 0xff)); 49 Buffer.add_char buffer (char_of_int ((byte_rate lsr 8) land 0xff)); 50 Buffer.add_char buffer (char_of_int ((byte_rate lsr 16) land 0xff)); 51 Buffer.add_char buffer (char_of_int ((byte_rate lsr 24) land 0xff)); 52 53 (* Block align *) 54 Buffer.add_char buffer (char_of_int block_align); 55 Buffer.add_char buffer (char_of_int 0); 56 57 (* Bits per sample *) 58 Buffer.add_char buffer (char_of_int bits_per_sample); 59 Buffer.add_char buffer (char_of_int 0); 60 61 (* "data" sub-chunk *) 62 Buffer.add_string buffer "data"; 63 Buffer.add_char buffer (char_of_int (data_size land 0xff)); 64 Buffer.add_char buffer (char_of_int ((data_size lsr 8) land 0xff)); 65 Buffer.add_char buffer (char_of_int ((data_size lsr 16) land 0xff)); 66 Buffer.add_char buffer (char_of_int ((data_size lsr 24) land 0xff)); 67 68 (* Generate sine wave data *) 69 let max_amplitude = float_of_int (1 lsl (bits_per_sample - 1)) -. 1.0 in 70 for i = 0 to num_samples - 1 do 71 let t = float_of_int i /. float_of_int sample_rate in 72 let value = int_of_float (amplitude *. max_amplitude *. sin (2.0 *. Float.pi *. frequency *. t)) in 73 (* Write 16-bit sample (little-endian) *) 74 Buffer.add_char buffer (char_of_int (value land 0xff)); 75 Buffer.add_char buffer (char_of_int ((value lsr 8) land 0xff)); 76 done; 77 78 Buffer.contents buffer 79 80 (* Encode binary data as base64 *) 81 let base64_encode data = 82 let buffer = Buffer.create (4 * (String.length data + 2) / 3) in 83 let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" in 84 85 let encode_block i bytes = 86 let b1 = Char.code (String.get bytes (i * 3)) in 87 let b2 = if i * 3 + 1 < String.length bytes then Char.code (String.get bytes (i * 3 + 1)) else 0 in 88 let b3 = if i * 3 + 2 < String.length bytes then Char.code (String.get bytes (i * 3 + 2)) else 0 in 89 90 let n = (b1 lsl 16) lor (b2 lsl 8) lor b3 in 91 Buffer.add_char buffer (String.get alphabet ((n lsr 18) land 63)); 92 Buffer.add_char buffer (String.get alphabet ((n lsr 12) land 63)); 93 94 if i * 3 + 1 < String.length bytes then 95 Buffer.add_char buffer (String.get alphabet ((n lsr 6) land 63)) 96 else 97 Buffer.add_char buffer '='; 98 99 if i * 3 + 2 < String.length bytes then 100 Buffer.add_char buffer (String.get alphabet (n land 63)) 101 else 102 Buffer.add_char buffer '='; 103 in 104 105 for i = 0 to (String.length data + 2) / 3 - 1 do 106 encode_block i data 107 done; 108 109 Buffer.contents buffer 110end 111 112(* Helper for extracting string value from JSON *) 113let get_string_param json name = 114 match json with 115 | `Assoc fields -> 116 (match List.assoc_opt name fields with 117 | Some (`String value) -> value 118 | _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name))) 119 | _ -> raise (Failure "Expected JSON object") 120 121(* Create a server *) 122let server = create_server 123 ~name:"OCaml MCP Audio Example" 124 ~version:"0.1.0" 125 ~protocol_version:"2024-11-05" 126 () 127 128(* Define startup and shutdown hooks *) 129let startup () = 130 (* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *) 131 Printf.fprintf stderr "AudioExampleServer is starting up!\n"; 132 flush stderr; 133 Log.info "AudioExampleServer is starting up!" 134 135let shutdown () = 136 Printf.fprintf stderr "AudioExampleServer is shutting down. Goodbye!\n"; 137 flush stderr; 138 Log.info "AudioExampleServer is shutting down. Goodbye!" 139 140(* Register the hooks *) 141let () = 142 set_startup_hook server startup; 143 set_shutdown_hook server shutdown 144 145(* Helper to create audio content *) 146let make_audio_content data mime_type = 147 let audio_content = AudioContent.{ 148 data; 149 mime_type; 150 annotations = None; 151 } in 152 Audio audio_content 153 154(* Define and register an audio tool *) 155let _ = add_tool server 156 ~name:"generate_audio_description" 157 ~description:"Generates a description with an audio sample" 158 ~schema_properties:[ 159 ("text", "string", "The text to describe with audio"); 160 ("frequency", "number", "The frequency in Hz for the tone (optional)"); 161 ("duration", "number", "The duration in seconds for the tone (optional)"); 162 ("amplitude", "number", "The amplitude (0.0-1.0) for the tone (optional)"); 163 ] 164 ~schema_required:["text"] 165 (fun args -> 166 try 167 let text = get_string_param args "text" in 168 169 (* Parse parameters with defaults *) 170 let frequency = 171 try 172 match List.assoc_opt "frequency" (match args with `Assoc l -> l | _ -> []) with 173 | Some (`Int f) -> float_of_int f 174 | Some (`Float f) -> f 175 | _ -> 440.0 (* Default to A440 *) 176 with _ -> 440.0 177 in 178 179 let duration = 180 try 181 match List.assoc_opt "duration" (match args with `Assoc l -> l | _ -> []) with 182 | Some (`Int d) -> float_of_int d 183 | Some (`Float d) -> d 184 | _ -> 2.0 (* Default to 2 seconds *) 185 with _ -> 2.0 186 in 187 188 let amplitude = 189 try 190 match List.assoc_opt "amplitude" (match args with `Assoc l -> l | _ -> []) with 191 | Some (`Int a) -> float_of_int a 192 | Some (`Float a) -> a 193 | _ -> 0.8 (* Default to 80% amplitude *) 194 with _ -> 0.8 195 in 196 197 (* Generate WAV file for the tone *) 198 let sample_rate = 44100 in (* CD quality *) 199 let wav_data = Wav.generate_sine_wave 200 ~frequency 201 ~duration 202 ~sample_rate 203 ~amplitude 204 in 205 206 (* Encode WAV data as base64 *) 207 let base64_audio = Wav.base64_encode wav_data in 208 209 Log.info (Printf.sprintf "Generated %d Hz tone for %.1f seconds (%.1f KB)" 210 (int_of_float frequency) duration 211 (float_of_int (String.length wav_data) /. 1024.0)); 212 213 (* Create a response with both text and audio content *) 214 CallToolResult.yojson_of_t CallToolResult.{ 215 content = [ 216 Text TextContent.{ 217 text = Printf.sprintf "Description: %s (with %.1f Hz tone for %.1f seconds)" 218 text frequency duration; 219 annotations = None 220 }; 221 Audio AudioContent.{ 222 data = base64_audio; 223 mime_type = "audio/wav"; 224 annotations = None 225 } 226 ]; 227 is_error = false; 228 meta = None 229 } 230 with 231 | Failure msg -> 232 Log.error (Printf.sprintf "Error in audio tool: %s" msg); 233 CallToolResult.yojson_of_t CallToolResult.{ 234 content = [ 235 Text TextContent.{ 236 text = Printf.sprintf "Error: %s" msg; 237 annotations = None 238 } 239 ]; 240 is_error = true; 241 meta = None 242 } 243 ) 244 245(* Define and register a prompt example with audio *) 246let _ = add_prompt server 247 ~name:"audio-description-prompt" 248 ~description:"A prompt with audio and text content" 249 ~arguments:[ 250 ("description", Some "Text description to accompany the audio", true); 251 ("frequency", Some "Frequency in Hz for the audio tone", false); 252 ("duration", Some "Duration in seconds for the audio tone", false); 253 ] 254 (fun args -> 255 let description = 256 try List.assoc "description" args 257 with Not_found -> "No description provided" 258 in 259 260 (* Parse frequency with default *) 261 let frequency = 262 try float_of_string (List.assoc "frequency" args) 263 with _ -> 440.0 (* Default to A440 *) 264 in 265 266 (* Parse duration with default *) 267 let duration = 268 try float_of_string (List.assoc "duration" args) 269 with _ -> 3.0 (* Default to 3 seconds *) 270 in 271 272 (* Generate WAV data *) 273 let sample_rate = 44100 in 274 let wav_data = Wav.generate_sine_wave 275 ~frequency 276 ~duration 277 ~sample_rate 278 ~amplitude:0.8 279 in 280 281 (* Encode WAV data as base64 *) 282 let base64_audio = Wav.base64_encode wav_data in 283 284 Log.info (Printf.sprintf "Generated %.1f Hz tone for prompt (%.1f seconds, %.1f KB)" 285 frequency duration 286 (float_of_int (String.length wav_data) /. 1024.0)); 287 288 [ 289 Prompt.{ 290 role = `User; 291 content = make_text_content "Here's a sound sample with description:" 292 }; 293 Prompt.{ 294 role = `User; 295 content = make_audio_content base64_audio "audio/wav" 296 }; 297 Prompt.{ 298 role = `User; 299 content = make_text_content (Printf.sprintf "%s (%.1f Hz tone for %.1f seconds)" 300 description frequency duration) 301 }; 302 Prompt.{ 303 role = `Assistant; 304 content = make_text_content "I've received your audio file and description." 305 } 306 ] 307 ) 308 309(* Main function *) 310let () = 311 (* Parse command line arguments *) 312 let transport_type = ref Mcp_server.Stdio in 313 let args = [ 314 ("--http", Arg.Unit (fun () -> transport_type := Mcp_server.Http), 315 "Start server with HTTP transport (default is stdio)"); 316 ] in 317 let usage_msg = "Usage: audio_example [--http]" in 318 Arg.parse args (fun _ -> ()) usage_msg; 319 320 (* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *) 321 Printf.fprintf stderr "Starting AudioExampleServer...\n"; 322 flush stderr; 323 Log.info "Starting AudioExampleServer..."; 324 325 (* Configure the server with appropriate capabilities *) 326 ignore (configure_server server ()); 327 328 (* Create and start MCP server with the selected transport *) 329 let mcp_server = Mcp_server.create ~server ~transport:!transport_type () in 330 Mcp_server.start mcp_server