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 random image as SVG format *)
51let generate_random_image width height =
52 (* Helper to get random color with param control *)
53 let random_color ?(min=0) ?(max=255) () =
54 let r = min + Random.int (max - min + 1) in
55 let g = min + Random.int (max - min + 1) in
56 let b = min + Random.int (max - min + 1) in
57 Printf.sprintf "#%02x%02x%02x" r g b
58 in
59
60 (* Create SVG header *)
61 let svg_buffer = Buffer.create 10240 in
62 Buffer.add_string svg_buffer (Printf.sprintf
63 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n\
64 <svg width=\"%d\" height=\"%d\" xmlns=\"http://www.w3.org/2000/svg\">\n\
65 <rect width=\"%d\" height=\"%d\" fill=\"%s\"/>\n"
66 width height width height (random_color ~min:200 ~max:240 ())
67 );
68
69 (* Generate different SVG shapes based on image size and randomness *)
70 let shape_count = (width * height) / 5000 + 5 in
71
72 (* Add random circles *)
73 for _ = 1 to shape_count / 2 do
74 let cx = Random.int width in
75 let cy = Random.int height in
76 let r = 5 + Random.int (min width height / 8) in
77 let color = random_color ~min:50 ~max:200 () in
78 let opacity = 0.3 +. (Random.float 0.7) in
79 Buffer.add_string svg_buffer (Printf.sprintf
80 "<circle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" fill-opacity=\"%.2f\" />\n"
81 cx cy r color opacity
82 );
83 done;
84
85 (* Add random rectangles *)
86 for _ = 1 to shape_count / 3 do
87 let x = Random.int width in
88 let y = Random.int height in
89 let w = 10 + Random.int (width / 5) in
90 let h = 10 + Random.int (height / 5) in
91 let color = random_color ~min:50 ~max:200 () in
92 let opacity = 0.2 +. (Random.float 0.6) in
93 let rx = 2 + Random.int 20 in (* Rounded corners *)
94 Buffer.add_string svg_buffer (Printf.sprintf
95 "<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"%d\" fill=\"%s\" fill-opacity=\"%.2f\" />\n"
96 x y w h rx color opacity
97 );
98 done;
99
100 (* Add random lines *)
101 for _ = 1 to shape_count do
102 let x1 = Random.int width in
103 let y1 = Random.int height in
104 let x2 = Random.int width in
105 let y2 = Random.int height in
106 let stroke = random_color () in
107 let sw = 1 + Random.int 5 in
108 Buffer.add_string svg_buffer (Printf.sprintf
109 "<line x1=\"%d\" y1=\"%d\" x2=\"%d\" y2=\"%d\" stroke=\"%s\" stroke-width=\"%d\" />\n"
110 x1 y1 x2 y2 stroke sw
111 );
112 done;
113
114 (* Add some random polygons *)
115 for _ = 1 to shape_count / 4 do
116 let points = 3 + Random.int 5 in (* 3 to 7 points *)
117 let cx = Random.int width in
118 let cy = Random.int height in
119 let radius = 10 + Random.int (min width height / 6) in
120 let points_str = Buffer.create 100 in
121
122 for i = 0 to points - 1 do
123 let angle = 2.0 *. Float.pi *. (float_of_int i) /. (float_of_int points) in
124 let px = cx + int_of_float (float_of_int radius *. cos angle) in
125 let py = cy + int_of_float (float_of_int radius *. sin angle) in
126 Buffer.add_string points_str (Printf.sprintf "%d,%d " px py);
127 done;
128
129 let fill = random_color ~min:100 ~max:220 () in
130 let opacity = 0.2 +. Random.float 0.5 in
131
132 Buffer.add_string svg_buffer (Printf.sprintf
133 "<polygon points=\"%s\" fill=\"%s\" fill-opacity=\"%.2f\" />\n"
134 (Buffer.contents points_str) fill opacity
135 );
136 done;
137
138 (* Close SVG tag *)
139 Buffer.add_string svg_buffer "</svg>";
140
141 (* Return the SVG directly, no need for Base64 since it's already text *)
142 Buffer.contents svg_buffer
143
144(* Helper to write 32-bit little endian integer *)
145let write_int32_le buf n =
146 Buffer.add_char buf (Char.chr (n land 0xff));
147 Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff));
148 Buffer.add_char buf (Char.chr ((n lsr 16) land 0xff));
149 Buffer.add_char buf (Char.chr ((n lsr 24) land 0xff))
150
151(* Helper to write 16-bit little endian integer *)
152let write_int16_le buf n =
153 Buffer.add_char buf (Char.chr (n land 0xff));
154 Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff))
155
156(* Generate a simple WAV file with sine wave *)
157let generate_sine_wave_audio frequency duration =
158 (* WAV header *)
159 let sample_rate = 8000 in
160 let num_samples = sample_rate * duration in
161 let header_buf = Buffer.create 44 in
162
163 (* Fill WAV header properly *)
164 Buffer.add_string header_buf "RIFF";
165 write_int32_le header_buf (36 + num_samples * 2); (* File size minus 8 *)
166 Buffer.add_string header_buf "WAVE";
167
168 (* Format chunk *)
169 Buffer.add_string header_buf "fmt ";
170 write_int32_le header_buf 16; (* Format chunk size *)
171 write_int16_le header_buf 1; (* PCM format *)
172 write_int16_le header_buf 1; (* Mono *)
173 write_int32_le header_buf sample_rate; (* Sample rate *)
174 write_int32_le header_buf (sample_rate * 2); (* Byte rate *)
175 write_int16_le header_buf 2; (* Block align *)
176 write_int16_le header_buf 16; (* Bits per sample *)
177
178 (* Data chunk *)
179 Buffer.add_string header_buf "data";
180 write_int32_le header_buf (num_samples * 2); (* Data size *)
181
182 (* Generate sine wave samples *)
183 let samples_buf = Buffer.create (num_samples * 2) in
184 let amplitude = 16384.0 in (* 16-bit with headroom *)
185
186 for i = 0 to num_samples - 1 do
187 let t = float_of_int i /. float_of_int sample_rate in
188 let value = amplitude *. sin (2.0 *. Float.pi *. frequency *. t) in
189 let sample = int_of_float value in
190
191 (* Convert to 16-bit little-endian *)
192 let sample = if sample < 0 then sample + 65536 else sample in
193 write_int16_le samples_buf sample;
194 done;
195
196 (* Combine header and samples, then encode as Base64 *)
197 let wav_data = Buffer.contents header_buf ^ Buffer.contents samples_buf in
198 Base64.encode wav_data
199
200(* Create a server *)
201let server = create_server
202 ~name:"OCaml MCP Multimodal Example"
203 ~version:"0.1.0"
204 ~protocol_version:"2025-03-26" () |>
205 fun server ->
206 (* Set default capabilities *)
207 configure_server server ~with_tools:true ~with_resources:true ~with_prompts:true ()
208
209(* Define and register a multimodal tool that returns text, images, and audio *)
210let _ = add_tool server
211 ~name:"multimodal_demo"
212 ~description:"Demonstrates multimodal content with text, image, and audio"
213 ~schema_properties:[
214 ("width", "integer", "Width of the generated image (pixels)");
215 ("height", "integer", "Height of the generated image (pixels)");
216 ("frequency", "integer", "Frequency of the generated audio tone (Hz)");
217 ("duration", "integer", "Duration of the generated audio (seconds)");
218 ("message", "string", "Text message to include")
219 ]
220 ~schema_required:["message"]
221 (fun args ->
222 try
223 (* Extract parameters with defaults if not provided *)
224 let message = get_string_param args "message" in
225 let width = try get_int_param args "width" with _ -> 128 in
226 let height = try get_int_param args "height" with _ -> 128 in
227 let frequency = try get_int_param args "frequency" with _ -> 440 in
228 let duration = try get_int_param args "duration" with _ -> 1 in
229
230 (* Generate image and audio data *)
231 let image_data = generate_random_image width height in
232 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in
233
234 (* Create a multimodal tool result *)
235 create_rich_tool_result
236 ~text:(Some message)
237 ~image:(Some (image_data, "image/svg+xml"))
238 ~audio:(Some (audio_data, "audio/wav"))
239 ~is_error:false
240 ()
241 with
242 | Failure msg ->
243 Log.error (Printf.sprintf "Error in multimodal tool: %s" msg);
244 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
245 )
246
247(* Define and register a tool for generating only images *)
248let _ = add_tool server
249 ~name:"generate_image"
250 ~description:"Generates a random image with specified dimensions"
251 ~schema_properties:[
252 ("width", "integer", "Width of the generated image (pixels)");
253 ("height", "integer", "Height of the generated image (pixels)")
254 ]
255 ~schema_required:["width"; "height"]
256 (fun args ->
257 try
258 let width = get_int_param args "width" in
259 let height = get_int_param args "height" in
260
261 if width < 1 || width > 1024 || height < 1 || height > 1024 then
262 create_tool_result
263 [TextContent "Error: Dimensions must be between 1 and 1024 pixels"]
264 ~is_error:true
265 else
266 let image_data = generate_random_image width height in
267 create_tool_result
268 [ImageContent { data = image_data; mime_type = "image/svg+xml" }]
269 ~is_error:false
270 with
271 | Failure msg ->
272 Log.error (Printf.sprintf "Error in generate_image tool: %s" msg);
273 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
274 )
275
276(* Define and register a tool for generating only audio *)
277let _ = add_tool server
278 ~name:"generate_audio"
279 ~description:"Generates an audio tone with specified frequency and duration"
280 ~schema_properties:[
281 ("frequency", "integer", "Frequency of the tone in Hz (20-20000)");
282 ("duration", "integer", "Duration of the tone in seconds (1-10)")
283 ]
284 ~schema_required:["frequency"; "duration"]
285 (fun args ->
286 try
287 let frequency = get_int_param args "frequency" in
288 let duration = get_int_param args "duration" in
289
290 if frequency < 20 || frequency > 20000 then
291 create_tool_result
292 [TextContent "Error: Frequency must be between 20Hz and 20,000Hz"]
293 ~is_error:true
294 else if duration < 1 || duration > 10 then
295 create_tool_result
296 [TextContent "Error: Duration must be between 1 and 10 seconds"]
297 ~is_error:true
298 else
299 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in
300 create_tool_result
301 [AudioContent { data = audio_data; mime_type = "audio/wav" }]
302 ~is_error:false
303 with
304 | Failure msg ->
305 Log.error (Printf.sprintf "Error in generate_audio tool: %s" msg);
306 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
307 )
308
309(* Define and register a resource example with multimodal content *)
310let _ = add_resource server
311 ~uri_template:"multimodal://{name}"
312 ~description:"Get a multimodal greeting with text, image and audio"
313 ~mime_type:"application/json"
314 (fun params ->
315 match params with
316 | [name] ->
317 let greeting = Printf.sprintf "Hello, %s! Welcome to the multimodal MCP example." name in
318 let image_data = generate_random_image 128 128 in
319 let audio_data = generate_sine_wave_audio 440.0 1 in
320
321 Printf.sprintf {|
322 {
323 "greeting": "%s",
324 "image": {
325 "data": "%s",
326 "mimeType": "image/svg+xml"
327 },
328 "audio": {
329 "data": "%s",
330 "mimeType": "audio/wav"
331 }
332 }
333 |} greeting image_data audio_data
334 | _ -> Printf.sprintf {|{"error": "Invalid parameters"}|}
335 )
336
337(* Run the server with the default scheduler *)
338let () =
339 Random.self_init(); (* Initialize random generator *)
340 Eio_main.run @@ fun env ->
341 Mcp_server.run_server env server