Model Context Protocol in OCaml
1open Mcp
2open Mcp_sdk
3
4(* Helper for extracting string value from JSON *)
5let get_string_param json name =
6 match json with
7 | `Assoc fields ->
8 (match List.assoc_opt name fields with
9 | Some (`String value) -> value
10 | _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name)))
11 | _ -> raise (Failure "Expected JSON object")
12
13(* Helper for extracting integer value from JSON *)
14let get_int_param json name =
15 match json with
16 | `Assoc fields ->
17 (match List.assoc_opt name fields with
18 | Some (`Int value) -> 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 PPM format (simple RGB format) *)
51let generate_random_image width height =
52 let header = Printf.sprintf "P6\n%d %d\n255\n" width height in
53 let data = Bytes.create (width * height * 3) in
54 let step = Random.int 20 + 10 in
55
56 for y = 0 to height - 1 do
57 for x = 0 to width - 1 do
58 let pattern_val = ((x / step) + (y / step)) mod 2 in
59 let offset = (y * width + x) * 3 in
60 if pattern_val = 0 then begin
61 (* Random bright color for checkerboard *)
62 let r = 150 + Random.int 100 in
63 let g = 150 + Random.int 100 in
64 let b = 150 + Random.int 100 in
65 Bytes.set data offset (Char.chr r);
66 Bytes.set data (offset + 1) (Char.chr g);
67 Bytes.set data (offset + 2) (Char.chr b);
68 end else begin
69 (* Dark color *)
70 let r = Random.int 100 in
71 let g = Random.int 100 in
72 let b = Random.int 100 in
73 Bytes.set data offset (Char.chr r);
74 Bytes.set data (offset + 1) (Char.chr g);
75 Bytes.set data (offset + 2) (Char.chr b);
76 end
77 done
78 done;
79
80 (* Encode PPM data as Base64 *)
81 Base64.encode (header ^ Bytes.to_string data)
82
83(* Generate a simple WAV file with sine wave *)
84let generate_sine_wave_audio frequency duration =
85 (* WAV header *)
86 let sample_rate = 8000 in
87 let num_samples = sample_rate * duration in
88 let header = "RIFF" ^
89 String.make 4 '\000' ^ (* Size placeholder *)
90 "WAVEfmt " ^
91 String.make 4 '\016' ^ (* Subchunk1 size *)
92 String.make 2 '\001' ^ (* Audio format = 1 (PCM) *)
93 String.make 2 '\001' ^ (* Num channels = 1 (mono) *)
94 String.make 4 (Char.chr sample_rate) ^ (* Sample rate as 4 bytes *)
95 String.make 4 (Char.chr (sample_rate * 2)) ^ (* Byte rate *)
96 String.make 2 '\002' ^ (* Block align *)
97 String.make 2 '\016' ^ (* Bits per sample = 16 *)
98 "data" ^
99 String.make 4 (Char.chr (num_samples * 2)) in (* Data size *)
100
101 (* Generate sine wave samples *)
102 let samples = Bytes.create (num_samples * 2) in
103 let amplitude = 16384.0 in (* 16-bit with headroom *)
104
105 for i = 0 to num_samples - 1 do
106 let t = float_of_int i /. float_of_int sample_rate in
107 let value = amplitude *. sin (2.0 *. Float.pi *. frequency *. t) in
108 let sample = int_of_float value in
109
110 (* Convert to 16-bit little-endian *)
111 let sample = if sample < 0 then sample + 65536 else sample in
112 Bytes.set samples (i * 2) (Char.chr (sample land 0xff));
113 Bytes.set samples (i * 2 + 1) (Char.chr ((sample lsr 8) land 0xff));
114 done;
115
116 (* Encode WAV data as Base64 *)
117 Base64.encode (header ^ Bytes.to_string samples)
118
119(* Create a server *)
120let server = create_server
121 ~name:"OCaml MCP Multimodal Example"
122 ~version:"0.1.0"
123 ~protocol_version:"2025-03-26" () |>
124 fun server ->
125 (* Set default capabilities *)
126 configure_server server ~with_tools:true ~with_resources:true ~with_prompts:true ()
127
128(* Define and register a multimodal tool that returns text, images, and audio *)
129let _ = add_tool server
130 ~name:"multimodal_demo"
131 ~description:"Demonstrates multimodal content with text, image, and audio"
132 ~schema_properties:[
133 ("width", "integer", "Width of the generated image (pixels)");
134 ("height", "integer", "Height of the generated image (pixels)");
135 ("frequency", "integer", "Frequency of the generated audio tone (Hz)");
136 ("duration", "integer", "Duration of the generated audio (seconds)");
137 ("message", "string", "Text message to include")
138 ]
139 ~schema_required:["message"]
140 (fun args ->
141 try
142 (* Extract parameters with defaults if not provided *)
143 let message = get_string_param args "message" in
144 let width = try get_int_param args "width" with _ -> 128 in
145 let height = try get_int_param args "height" with _ -> 128 in
146 let frequency = try get_int_param args "frequency" with _ -> 440 in
147 let duration = try get_int_param args "duration" with _ -> 1 in
148
149 (* Generate image and audio data *)
150 let image_data = generate_random_image width height in
151 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in
152
153 (* Create a multimodal tool result *)
154 create_rich_tool_result
155 ~text:(Some message)
156 ~image:(Some (image_data, "image/ppm"))
157 ~audio:(Some (audio_data, "audio/wav"))
158 ~is_error:false
159 ()
160 with
161 | Failure msg ->
162 Log.error (Printf.sprintf "Error in multimodal tool: %s" msg);
163 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
164 )
165
166(* Define and register a tool for generating only images *)
167let _ = add_tool server
168 ~name:"generate_image"
169 ~description:"Generates a random image with specified dimensions"
170 ~schema_properties:[
171 ("width", "integer", "Width of the generated image (pixels)");
172 ("height", "integer", "Height of the generated image (pixels)")
173 ]
174 ~schema_required:["width"; "height"]
175 (fun args ->
176 try
177 let width = get_int_param args "width" in
178 let height = get_int_param args "height" in
179
180 if width < 1 || width > 1024 || height < 1 || height > 1024 then
181 create_tool_result
182 [TextContent "Error: Dimensions must be between 1 and 1024 pixels"]
183 ~is_error:true
184 else
185 let image_data = generate_random_image width height in
186 create_tool_result
187 [ImageContent { data = image_data; mime_type = "image/ppm" }]
188 ~is_error:false
189 with
190 | Failure msg ->
191 Log.error (Printf.sprintf "Error in generate_image tool: %s" msg);
192 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
193 )
194
195(* Define and register a tool for generating only audio *)
196let _ = add_tool server
197 ~name:"generate_audio"
198 ~description:"Generates an audio tone with specified frequency and duration"
199 ~schema_properties:[
200 ("frequency", "integer", "Frequency of the tone in Hz (20-20000)");
201 ("duration", "integer", "Duration of the tone in seconds (1-10)")
202 ]
203 ~schema_required:["frequency"; "duration"]
204 (fun args ->
205 try
206 let frequency = get_int_param args "frequency" in
207 let duration = get_int_param args "duration" in
208
209 if frequency < 20 || frequency > 20000 then
210 create_tool_result
211 [TextContent "Error: Frequency must be between 20Hz and 20,000Hz"]
212 ~is_error:true
213 else if duration < 1 || duration > 10 then
214 create_tool_result
215 [TextContent "Error: Duration must be between 1 and 10 seconds"]
216 ~is_error:true
217 else
218 let audio_data = generate_sine_wave_audio (float_of_int frequency) duration in
219 create_tool_result
220 [AudioContent { data = audio_data; mime_type = "audio/wav" }]
221 ~is_error:false
222 with
223 | Failure msg ->
224 Log.error (Printf.sprintf "Error in generate_audio tool: %s" msg);
225 create_tool_result [TextContent (Printf.sprintf "Error: %s" msg)] ~is_error:true
226 )
227
228(* Define and register a resource example with multimodal content *)
229let _ = add_resource server
230 ~uri_template:"multimodal://{name}"
231 ~description:"Get a multimodal greeting with text, image and audio"
232 ~mime_type:"application/json"
233 (fun params ->
234 match params with
235 | [name] ->
236 let greeting = Printf.sprintf "Hello, %s! Welcome to the multimodal MCP example." name in
237 let image_data = generate_random_image 128 128 in
238 let audio_data = generate_sine_wave_audio 440.0 1 in
239
240 Printf.sprintf {|
241 {
242 "greeting": "%s",
243 "image": {
244 "data": "%s",
245 "mimeType": "image/ppm"
246 },
247 "audio": {
248 "data": "%s",
249 "mimeType": "audio/wav"
250 }
251 }
252 |} greeting image_data audio_data
253 | _ -> Printf.sprintf {|{"error": "Invalid parameters"}|}
254 )
255
256(* Run the server with the default scheduler *)
257let () =
258 Random.self_init(); (* Initialize random generator *)
259 Eio_main.run @@ fun env ->
260 Mcp_server.run_server env server