Model Context Protocol in OCaml

tmo

+13 -2
bin/audio_example.ml
···
open Mcp
open Mcp_sdk
(* WAV file format helper module *)
module Wav = struct
···
(* Main function *)
let () =
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting AudioExampleServer...\n";
flush stderr;
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
-
(* Run the server *)
-
run_server server
···
open Mcp
open Mcp_sdk
+
open Mcp_server
(* WAV file format helper module *)
module Wav = struct
···
(* Main function *)
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Mcp_server.Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Mcp_server.Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: audio_example [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting AudioExampleServer...\n";
flush stderr;
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = Mcp_server.create ~server ~transport:!transport_type () in
+
Mcp_server.start mcp_server
+13 -2
bin/capitalize_sdk.ml
···
open Mcp
open Mcp_sdk
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting CapitalizeServer...\n";
flush stderr;
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
-
(* Run the server *)
-
run_server server
···
open Mcp
open Mcp_sdk
+
open Mcp_server
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: capitalize_sdk [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting CapitalizeServer...\n";
flush stderr;
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server
+13 -2
bin/completion_example.ml
···
open Mcp
open Mcp_sdk
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting CompletionServer...\n";
flush stderr;
···
] in
set_capabilities server capabilities;
-
(* Run the server *)
-
run_server server
···
open Mcp
open Mcp_sdk
+
open Mcp_server
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: completion_example [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
Printf.fprintf stderr "Starting CompletionServer...\n";
flush stderr;
···
] in
set_capabilities server capabilities;
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server
+24 -5
bin/dune
···
(executable
(name server)
-
(libraries mcp yojson unix))
(executable
(name capitalize_sdk)
(modules capitalize_sdk)
-
(libraries mcp mcp_sdk yojson unix))
(executable
(name audio_example)
(modules audio_example)
-
(libraries mcp mcp_sdk yojson unix))
(executable
(name resource_template_example)
(modules resource_template_example)
-
(libraries mcp mcp_sdk yojson unix))
(executable
(name completion_example)
(modules completion_example)
-
(libraries mcp mcp_sdk yojson unix))
···
(executable
(name server)
+
(libraries mcp yojson unix)
+
(flags (:standard -w -8-11)))
(executable
(name capitalize_sdk)
(modules capitalize_sdk)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33)))
(executable
(name audio_example)
(modules audio_example)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33)))
(executable
(name resource_template_example)
(modules resource_template_example)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33)))
(executable
(name completion_example)
(modules completion_example)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33)))
+
+
+
(executable
+
(name image_generator_example)
+
(modules image_generator_example)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33)))
+
+
(executable
+
(name multimodal_example)
+
(modules multimodal_example)
+
(libraries mcp mcp_sdk mcp_server yojson unix)
+
(flags (:standard -w -33 -w -32)))
+
+468
bin/image_generator_example.ml
···
···
+
open Mcp
+
open Mcp_sdk
+
open Mcp_server
+
+
(* Random pixel image generator MCP server *)
+
+
(* Base64 encoding helper *)
+
module Base64 = struct
+
let encode_char n =
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".[n]
+
+
let encode_block i bytes =
+
let buffer = Buffer.create 4 in
+
let b1 = Char.code (String.get bytes (i * 3)) in
+
let b2 = if i * 3 + 1 < String.length bytes then Char.code (String.get bytes (i * 3 + 1)) else 0 in
+
let b3 = if i * 3 + 2 < String.length bytes then Char.code (String.get bytes (i * 3 + 2)) else 0 in
+
+
let n = (b1 lsl 16) lor (b2 lsl 8) lor b3 in
+
Buffer.add_char buffer (encode_char ((n lsr 18) land 63));
+
Buffer.add_char buffer (encode_char ((n lsr 12) land 63));
+
+
if i * 3 + 1 < String.length bytes then
+
Buffer.add_char buffer (encode_char ((n lsr 6) land 63))
+
else
+
Buffer.add_char buffer '=';
+
+
if i * 3 + 2 < String.length bytes then
+
Buffer.add_char buffer (encode_char (n land 63))
+
else
+
Buffer.add_char buffer '=';
+
+
Buffer.contents buffer
+
+
let encode data =
+
let buffer = Buffer.create (4 * (String.length data + 2) / 3) in
+
for i = 0 to (String.length data - 1) / 3 do
+
Buffer.add_string buffer (encode_block i data)
+
done;
+
Buffer.contents buffer
+
end
+
+
(* Image generation utilities *)
+
module ImageGenerator = struct
+
(* Simple PNG generation *)
+
let create_png width height pixels =
+
(* PNG signature *)
+
let signature = [|137; 80; 78; 71; 13; 10; 26; 10|] in
+
+
(* IHDR chunk data *)
+
let ihdr_data = Bytes.create 13 in
+
(* Width - big endian *)
+
Bytes.set ihdr_data 0 (Char.chr ((width lsr 24) land 0xff));
+
Bytes.set ihdr_data 1 (Char.chr ((width lsr 16) land 0xff));
+
Bytes.set ihdr_data 2 (Char.chr ((width lsr 8) land 0xff));
+
Bytes.set ihdr_data 3 (Char.chr (width land 0xff));
+
(* Height - big endian *)
+
Bytes.set ihdr_data 4 (Char.chr ((height lsr 24) land 0xff));
+
Bytes.set ihdr_data 5 (Char.chr ((height lsr 16) land 0xff));
+
Bytes.set ihdr_data 6 (Char.chr ((height lsr 8) land 0xff));
+
Bytes.set ihdr_data 7 (Char.chr (height land 0xff));
+
(* Bit depth - 8 bits *)
+
Bytes.set ihdr_data 8 (Char.chr 8);
+
(* Color type - RGB with alpha *)
+
Bytes.set ihdr_data 9 (Char.chr 6);
+
(* Compression, filter, interlace - all 0 *)
+
Bytes.set ihdr_data 10 (Char.chr 0);
+
Bytes.set ihdr_data 11 (Char.chr 0);
+
Bytes.set ihdr_data 12 (Char.chr 0);
+
+
(* Very simple CRC32 implementation for PNG chunks *)
+
let calculate_crc data =
+
let crc = ref 0xffffffff in
+
for i = 0 to Bytes.length data - 1 do
+
let byte = Char.code (Bytes.get data i) in
+
crc := !crc lxor byte;
+
for _ = 0 to 7 do
+
if !crc land 1 <> 0 then
+
crc := (!crc lsr 1) lxor 0xedb88320
+
else
+
crc := !crc lsr 1
+
done
+
done;
+
!crc lxor 0xffffffff
+
in
+
+
(* Create IHDR chunk *)
+
let ihdr_chunk = Buffer.create 25 in
+
(* Length - 13 bytes *)
+
Buffer.add_char ihdr_chunk (Char.chr 0);
+
Buffer.add_char ihdr_chunk (Char.chr 0);
+
Buffer.add_char ihdr_chunk (Char.chr 0);
+
Buffer.add_char ihdr_chunk (Char.chr 13);
+
(* Chunk type - IHDR *)
+
Buffer.add_string ihdr_chunk "IHDR";
+
(* Chunk data *)
+
Buffer.add_string ihdr_chunk (Bytes.unsafe_to_string ihdr_data);
+
(* CRC *)
+
let ihdr_crc_data = Bytes.create 17 in
+
Bytes.blit_string "IHDR" 0 ihdr_crc_data 0 4;
+
Bytes.blit ihdr_data 0 ihdr_crc_data 4 13;
+
let crc = calculate_crc ihdr_crc_data in
+
Buffer.add_char ihdr_chunk (Char.chr ((crc lsr 24) land 0xff));
+
Buffer.add_char ihdr_chunk (Char.chr ((crc lsr 16) land 0xff));
+
Buffer.add_char ihdr_chunk (Char.chr ((crc lsr 8) land 0xff));
+
Buffer.add_char ihdr_chunk (Char.chr (crc land 0xff));
+
+
(* Create IDAT chunk (uncompressed for simplicity) *)
+
let row_size = width * 4 in
+
let data_size = height * (row_size + 1) in
+
let idat_chunk = Buffer.create (12 + data_size) in
+
(* Length *)
+
Buffer.add_char idat_chunk (Char.chr ((data_size lsr 24) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((data_size lsr 16) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((data_size lsr 8) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr (data_size land 0xff));
+
(* Chunk type - IDAT *)
+
Buffer.add_string idat_chunk "IDAT";
+
+
(* Very simple zlib header (no compression) *)
+
Buffer.add_char idat_chunk (Char.chr 0x78); (* CMF byte *)
+
Buffer.add_char idat_chunk (Char.chr 0x01); (* FLG byte *)
+
+
(* Raw image data with filter type 0 (None) for each scanline *)
+
for y = 0 to height - 1 do
+
(* Filter type 0 (None) *)
+
Buffer.add_char idat_chunk (Char.chr 0);
+
for x = 0 to width - 1 do
+
let idx = (y * width + x) * 4 in
+
Buffer.add_char idat_chunk (Char.chr (Bytes.get_uint8 pixels idx)); (* R *)
+
Buffer.add_char idat_chunk (Char.chr (Bytes.get_uint8 pixels (idx + 1))); (* G *)
+
Buffer.add_char idat_chunk (Char.chr (Bytes.get_uint8 pixels (idx + 2))); (* B *)
+
Buffer.add_char idat_chunk (Char.chr (Bytes.get_uint8 pixels (idx + 3))); (* A *)
+
done
+
done;
+
+
(* Zlib Adler-32 checksum (simplified) *)
+
let adler = ref 1 in
+
Buffer.add_char idat_chunk (Char.chr ((!adler lsr 24) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((!adler lsr 16) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((!adler lsr 8) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr (!adler land 0xff));
+
+
(* CRC *)
+
let idat_crc = ref 0 in (* Not calculating CRC for simplicity *)
+
Buffer.add_char idat_chunk (Char.chr ((!idat_crc lsr 24) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((!idat_crc lsr 16) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr ((!idat_crc lsr 8) land 0xff));
+
Buffer.add_char idat_chunk (Char.chr (!idat_crc land 0xff));
+
+
(* Create IEND chunk *)
+
let iend_chunk = Buffer.create 12 in
+
(* Length - 0 bytes *)
+
Buffer.add_char iend_chunk (Char.chr 0);
+
Buffer.add_char iend_chunk (Char.chr 0);
+
Buffer.add_char iend_chunk (Char.chr 0);
+
Buffer.add_char iend_chunk (Char.chr 0);
+
(* Chunk type - IEND *)
+
Buffer.add_string iend_chunk "IEND";
+
(* CRC *)
+
let iend_crc = 0xAE426082 in (* Precomputed CRC for IEND chunk *)
+
Buffer.add_char iend_chunk (Char.chr ((iend_crc lsr 24) land 0xff));
+
Buffer.add_char iend_chunk (Char.chr ((iend_crc lsr 16) land 0xff));
+
Buffer.add_char iend_chunk (Char.chr ((iend_crc lsr 8) land 0xff));
+
Buffer.add_char iend_chunk (Char.chr (iend_crc land 0xff));
+
+
(* Combine all parts *)
+
let result = Buffer.create (8 + Buffer.length ihdr_chunk + Buffer.length idat_chunk + Buffer.length iend_chunk) in
+
(* PNG signature *)
+
Array.iter (fun c -> Buffer.add_char result (Char.chr c)) signature;
+
(* IHDR chunk *)
+
Buffer.add_buffer result ihdr_chunk;
+
(* IDAT chunk *)
+
Buffer.add_buffer result idat_chunk;
+
(* IEND chunk *)
+
Buffer.add_buffer result iend_chunk;
+
+
Buffer.contents result
+
+
(* Generate random pixel art image *)
+
let generate_random_image ?(width=16) ?(height=16) ?(pixel_size=1) ?(seed=None) () =
+
let pixels = Bytes.create (width * height * 4) in
+
+
(* Set random seed if provided *)
+
(match seed with
+
| Some s -> Random.init s
+
| None -> Random.self_init ());
+
+
(* Generate a random color palette *)
+
let palette_size = Random.int 8 + 2 in (* 2-10 colors *)
+
let palette = Array.init palette_size (fun _ ->
+
(Random.int 256, Random.int 256, Random.int 256, 255) (* RGBA *)
+
) in
+
+
(* Fill the pixel buffer *)
+
for y = 0 to height - 1 do
+
for x = 0 to width - 1 do
+
let color_idx = Random.int palette_size in
+
let (r, g, b, a) = palette.(color_idx) in
+
let idx = (y * width + x) * 4 in
+
Bytes.set_uint8 pixels idx r;
+
Bytes.set_uint8 pixels (idx + 1) g;
+
Bytes.set_uint8 pixels (idx + 2) b;
+
Bytes.set_uint8 pixels (idx + 3) a;
+
done
+
done;
+
+
(* Create symmetrical patterns - horizontally, vertically, or both *)
+
let symmetry_type = Random.int 3 in
+
if symmetry_type > 0 then begin
+
for y = 0 to height - 1 do
+
for x = 0 to width / 2 do
+
(* Mirror horizontally (except center column for odd widths) *)
+
if symmetry_type = 1 || symmetry_type = 2 then begin
+
let mirror_x = width - 1 - x in
+
if x <> mirror_x then begin
+
let src_idx = (y * width + x) * 4 in
+
let dst_idx = (y * width + mirror_x) * 4 in
+
for i = 0 to 3 do
+
Bytes.set pixels (dst_idx + i) (Bytes.get pixels (src_idx + i))
+
done
+
end
+
end
+
done
+
done;
+
+
(* Mirror vertically for symmetry_type = 2 *)
+
if symmetry_type = 2 then begin
+
for y = 0 to height / 2 do
+
let mirror_y = height - 1 - y in
+
if y <> mirror_y then begin
+
for x = 0 to width - 1 do
+
let src_idx = (y * width + x) * 4 in
+
let dst_idx = (mirror_y * width + x) * 4 in
+
for i = 0 to 3 do
+
Bytes.set pixels (dst_idx + i) (Bytes.get pixels (src_idx + i))
+
done
+
done
+
end
+
done
+
end
+
end;
+
+
(* Scale up the image if pixel_size > 1 *)
+
let final_width = width * pixel_size in
+
let final_height = height * pixel_size in
+
+
if pixel_size = 1 then
+
create_png width height pixels
+
else begin
+
let scaled_pixels = Bytes.create (final_width * final_height * 4) in
+
+
for y = 0 to height - 1 do
+
for x = 0 to width - 1 do
+
let src_idx = (y * width + x) * 4 in
+
for py = 0 to pixel_size - 1 do
+
for px = 0 to pixel_size - 1 do
+
let dst_x = x * pixel_size + px in
+
let dst_y = y * pixel_size + py in
+
let dst_idx = (dst_y * final_width + dst_x) * 4 in
+
for i = 0 to 3 do
+
Bytes.set scaled_pixels (dst_idx + i) (Bytes.get pixels (src_idx + i))
+
done
+
done
+
done
+
done
+
done;
+
+
create_png final_width final_height scaled_pixels
+
end
+
end
+
+
(* Helper for extracting values from JSON *)
+
let get_param_int json name default =
+
match json with
+
| `Assoc fields ->
+
(match List.assoc_opt name fields with
+
| Some (`Int i) -> i
+
| Some (`Float f) -> int_of_float f
+
| _ -> default)
+
| _ -> default
+
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Image Generator"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
Printf.fprintf stderr "ImageGeneratorServer is starting up!\n";
+
flush stderr;
+
Log.info "ImageGeneratorServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "ImageGeneratorServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "ImageGeneratorServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Make an image content helper *)
+
let make_image_content data mime_type =
+
let image_content = ImageContent.{
+
data;
+
mime_type;
+
annotations = None;
+
} in
+
Image image_content
+
+
(* Define and register a random pixel art generator tool *)
+
let _ = add_tool server
+
~name:"generate_random_pixel_art"
+
~description:"Generates a random pixel art image"
+
~schema_properties:[
+
("width", "integer", "Width of the pixel art grid (default: 16)");
+
("height", "integer", "Height of the pixel art grid (default: 16)");
+
("pixel_size", "integer", "Size of each pixel (default: 8)");
+
("seed", "integer", "Random seed (optional)");
+
]
+
~schema_required:[]
+
(fun args ->
+
try
+
let width = get_param_int args "width" 16 in
+
let height = get_param_int args "height" 16 in
+
let pixel_size = get_param_int args "pixel_size" 8 in
+
+
(* Validate parameters *)
+
let width = max 1 (min 64 width) in (* Limit to 1-64 *)
+
let height = max 1 (min 64 height) in (* Limit to 1-64 *)
+
let pixel_size = max 1 (min 16 pixel_size) in (* Limit to 1-16 *)
+
+
(* Extract optional seed *)
+
let seed = match args with
+
| `Assoc fields ->
+
(match List.assoc_opt "seed" fields with
+
| Some (`Int s) -> Some s
+
| _ -> None)
+
| _ -> None
+
in
+
+
(* Generate the image *)
+
let image_data = ImageGenerator.generate_random_image
+
~width ~height ~pixel_size ~seed () in
+
+
(* Encode as base64 *)
+
let base64_data = Base64.encode image_data in
+
+
Log.info (Printf.sprintf "Generated random pixel art image (%dx%d grid, %dpx pixels)"
+
width height pixel_size);
+
+
(* Create a response with both text and image content *)
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{
+
text = Printf.sprintf "Generated random pixel art image (%dx%d grid, %dpx pixels)"
+
width height pixel_size;
+
annotations = None
+
};
+
Image ImageContent.{
+
data = base64_data;
+
mime_type = "image/png";
+
annotations = None
+
}
+
];
+
is_error = false;
+
meta = None
+
}
+
with
+
| Failure msg ->
+
Log.error (Printf.sprintf "Error in image generator tool: %s" msg);
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{
+
text = Printf.sprintf "Error: %s" msg;
+
annotations = None
+
}
+
];
+
is_error = true;
+
meta = None
+
}
+
)
+
+
(* Define and register a pixel art prompt *)
+
let _ = add_prompt server
+
~name:"pixel-art-prompt"
+
~description:"A prompt that includes a random pixel art image"
+
~arguments:[
+
("width", Some "Width of the pixel art (1-64)", false);
+
("height", Some "Height of the pixel art (1-64)", false);
+
("pixel_size", Some "Size of each pixel (1-16)", false);
+
]
+
(fun args ->
+
(* Parse parameters with defaults *)
+
let width =
+
try int_of_string (List.assoc "width" args)
+
with _ -> 16
+
in
+
let height =
+
try int_of_string (List.assoc "height" args)
+
with _ -> 16
+
in
+
let pixel_size =
+
try int_of_string (List.assoc "pixel_size" args)
+
with _ -> 8
+
in
+
+
(* Validate parameters *)
+
let width = max 1 (min 64 width) in
+
let height = max 1 (min 64 height) in
+
let pixel_size = max 1 (min 16 pixel_size) in
+
+
(* Generate image *)
+
let image_data = ImageGenerator.generate_random_image
+
~width ~height ~pixel_size () in
+
+
(* Encode as base64 *)
+
let base64_data = Base64.encode image_data in
+
+
Log.info (Printf.sprintf "Generated pixel art for prompt (%dx%d grid, %dpx pixels)"
+
width height pixel_size);
+
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content "I've generated a random pixel art image for you:"
+
};
+
Prompt.{
+
role = `User;
+
content = make_image_content base64_data "image/png"
+
};
+
Prompt.{
+
role = `User;
+
content = make_text_content (Printf.sprintf "Please describe what you see in this %dx%d pixel art."
+
width height)
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content "I'll describe what I see in this pixel art image."
+
}
+
]
+
)
+
+
(* Main function *)
+
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: image_generator_example [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
+
Printf.fprintf stderr "Starting ImageGeneratorServer...\n";
+
flush stderr;
+
Log.info "Starting ImageGeneratorServer...";
+
+
(* Configure the server with appropriate capabilities *)
+
ignore (configure_server server ());
+
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server
+502
bin/multimodal_example.ml
···
···
+
open Mcp
+
open Mcp_sdk
+
open Mcp_server
+
+
(* Multimodal example MCP server *)
+
+
(* Base64 encoding helper *)
+
module Base64 = struct
+
let encode_char n =
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".[n]
+
+
let encode_block i bytes =
+
let buffer = Buffer.create 4 in
+
let b1 = Char.code (String.get bytes (i * 3)) in
+
let b2 = if i * 3 + 1 < String.length bytes then Char.code (String.get bytes (i * 3 + 1)) else 0 in
+
let b3 = if i * 3 + 2 < String.length bytes then Char.code (String.get bytes (i * 3 + 2)) else 0 in
+
+
let n = (b1 lsl 16) lor (b2 lsl 8) lor b3 in
+
Buffer.add_char buffer (encode_char ((n lsr 18) land 63));
+
Buffer.add_char buffer (encode_char ((n lsr 12) land 63));
+
+
if i * 3 + 1 < String.length bytes then
+
Buffer.add_char buffer (encode_char ((n lsr 6) land 63))
+
else
+
Buffer.add_char buffer '=';
+
+
if i * 3 + 2 < String.length bytes then
+
Buffer.add_char buffer (encode_char (n land 63))
+
else
+
Buffer.add_char buffer '=';
+
+
Buffer.contents buffer
+
+
let encode data =
+
let buffer = Buffer.create (4 * (String.length data + 2) / 3) in
+
for i = 0 to (String.length data - 1) / 3 do
+
Buffer.add_string buffer (encode_block i data)
+
done;
+
Buffer.contents buffer
+
end
+
+
(* Audio generator *)
+
module AudioGenerator = struct
+
(* Generate a simple sine wave *)
+
let generate_sine_wave ~frequency ~duration ~sample_rate ~amplitude =
+
(* WAV parameters *)
+
let num_channels = 1 in (* Mono *)
+
let bits_per_sample = 16 in
+
let byte_rate = sample_rate * num_channels * bits_per_sample / 8 in
+
let block_align = num_channels * bits_per_sample / 8 in
+
let num_samples = int_of_float (float_of_int sample_rate *. duration) in
+
let data_size = num_samples * block_align in
+
+
(* Create buffer for the WAV data *)
+
let buffer = Buffer.create (44 + data_size) in
+
+
(* Write WAV header *)
+
(* "RIFF" chunk *)
+
Buffer.add_string buffer "RIFF";
+
let file_size = 36 + data_size in
+
Buffer.add_char buffer (Char.chr (file_size land 0xff));
+
Buffer.add_char buffer (Char.chr ((file_size lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr ((file_size lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((file_size lsr 24) land 0xff));
+
Buffer.add_string buffer "WAVE";
+
+
(* "fmt " sub-chunk *)
+
Buffer.add_string buffer "fmt ";
+
Buffer.add_char buffer (Char.chr 16); (* Sub-chunk size (16 for PCM) *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 1); (* Audio format (1 for PCM) *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr num_channels); (* Number of channels *)
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* Sample rate *)
+
Buffer.add_char buffer (Char.chr (sample_rate land 0xff));
+
Buffer.add_char buffer (Char.chr ((sample_rate lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr ((sample_rate lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((sample_rate lsr 24) land 0xff));
+
+
(* Byte rate *)
+
Buffer.add_char buffer (Char.chr (byte_rate land 0xff));
+
Buffer.add_char buffer (Char.chr ((byte_rate lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr ((byte_rate lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((byte_rate lsr 24) land 0xff));
+
+
(* Block align *)
+
Buffer.add_char buffer (Char.chr block_align);
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* Bits per sample *)
+
Buffer.add_char buffer (Char.chr bits_per_sample);
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* "data" sub-chunk *)
+
Buffer.add_string buffer "data";
+
Buffer.add_char buffer (Char.chr (data_size land 0xff));
+
Buffer.add_char buffer (Char.chr ((data_size lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr ((data_size lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((data_size lsr 24) land 0xff));
+
+
(* Generate sine wave data *)
+
let max_amplitude = float_of_int (1 lsl (bits_per_sample - 1)) -. 1.0 in
+
for i = 0 to num_samples - 1 do
+
let t = float_of_int i /. float_of_int sample_rate in
+
let value = int_of_float (amplitude *. max_amplitude *. sin (2.0 *. Float.pi *. frequency *. t)) in
+
(* Write 16-bit sample (little-endian) *)
+
Buffer.add_char buffer (Char.chr (value land 0xff));
+
Buffer.add_char buffer (Char.chr ((value lsr 8) land 0xff));
+
done;
+
+
Buffer.contents buffer
+
end
+
+
(* Image generator *)
+
module ImageGenerator = struct
+
(* Simple PNG generation *)
+
let generate_simple_image width height color_str =
+
(* Parse color - expected format: #RRGGBB or #RRGGBBAA *)
+
let r, g, b, a =
+
try
+
if String.length color_str >= 7 && color_str.[0] = '#' then
+
let r = int_of_string ("0x" ^ String.sub color_str 1 2) in
+
let g = int_of_string ("0x" ^ String.sub color_str 3 2) in
+
let b = int_of_string ("0x" ^ String.sub color_str 5 2) in
+
let a = if String.length color_str >= 9 then
+
int_of_string ("0x" ^ String.sub color_str 7 2)
+
else 255 in
+
(r, g, b, a)
+
else
+
(255, 0, 0, 255) (* Default to red if invalid *)
+
with _ ->
+
(255, 0, 0, 255) (* Default to red on parsing error *)
+
in
+
+
(* Create a very simple 1x1 PNG with the specified color *)
+
(* PNG signature *)
+
let signature = [137; 80; 78; 71; 13; 10; 26; 10] in
+
+
(* Create buffer for the PNG data *)
+
let buffer = Buffer.create 100 in
+
+
(* PNG signature *)
+
List.iter (fun b -> Buffer.add_char buffer (Char.chr b)) signature;
+
+
(* IHDR chunk *)
+
Buffer.add_char buffer (Char.chr 0); (* length - 13 bytes *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 13);
+
+
Buffer.add_string buffer "IHDR";
+
+
(* Width *)
+
Buffer.add_char buffer (Char.chr ((width lsr 24) land 0xff));
+
Buffer.add_char buffer (Char.chr ((width lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((width lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr (width land 0xff));
+
+
(* Height *)
+
Buffer.add_char buffer (Char.chr ((height lsr 24) land 0xff));
+
Buffer.add_char buffer (Char.chr ((height lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((height lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr (height land 0xff));
+
+
Buffer.add_char buffer (Char.chr 8); (* Bit depth - 8 bits per channel *)
+
Buffer.add_char buffer (Char.chr 6); (* Color type - RGBA *)
+
Buffer.add_char buffer (Char.chr 0); (* Compression method - deflate *)
+
Buffer.add_char buffer (Char.chr 0); (* Filter method - adaptive filtering *)
+
Buffer.add_char buffer (Char.chr 0); (* Interlace method - no interlace *)
+
+
(* IHDR CRC - precomputed for simplicity *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* IDAT chunk - simplified for example *)
+
let pixels_per_row = width * 4 in
+
let data_size = (1 + pixels_per_row) * height in
+
+
Buffer.add_char buffer (Char.chr ((data_size lsr 24) land 0xff));
+
Buffer.add_char buffer (Char.chr ((data_size lsr 16) land 0xff));
+
Buffer.add_char buffer (Char.chr ((data_size lsr 8) land 0xff));
+
Buffer.add_char buffer (Char.chr (data_size land 0xff));
+
+
Buffer.add_string buffer "IDAT";
+
+
(* Simple zlib header *)
+
Buffer.add_char buffer (Char.chr 0x78);
+
Buffer.add_char buffer (Char.chr 0x01);
+
+
(* Raw image data *)
+
for _ = 0 to height - 1 do
+
Buffer.add_char buffer (Char.chr 0); (* Filter type 0 - None *)
+
for _ = 0 to width - 1 do
+
Buffer.add_char buffer (Char.chr r);
+
Buffer.add_char buffer (Char.chr g);
+
Buffer.add_char buffer (Char.chr b);
+
Buffer.add_char buffer (Char.chr a);
+
done
+
done;
+
+
(* Dummy Adler32 checksum *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* IDAT CRC - precomputed for simplicity *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
+
(* IEND chunk *)
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
Buffer.add_char buffer (Char.chr 0);
+
+
Buffer.add_string buffer "IEND";
+
+
(* IEND CRC - precomputed value *)
+
Buffer.add_char buffer (Char.chr 0xAE);
+
Buffer.add_char buffer (Char.chr 0x42);
+
Buffer.add_char buffer (Char.chr 0x60);
+
Buffer.add_char buffer (Char.chr 0x82);
+
+
Buffer.contents buffer
+
end
+
+
(* Helper for extracting values from JSON *)
+
let get_param_int json name default =
+
match json with
+
| `Assoc fields -> begin
+
match List.assoc_opt name fields with
+
| Some (`Int i) -> begin
+
i
+
end
+
| Some (`Float f) -> begin
+
int_of_float f
+
end
+
| _ -> begin
+
default
+
end
+
end
+
| _ -> begin
+
default
+
end
+
+
let get_param_float json name default =
+
match json with
+
| `Assoc fields -> begin
+
match List.assoc_opt name fields with
+
| Some (`Int i) -> begin
+
float_of_int i
+
end
+
| Some (`Float f) -> begin
+
f
+
end
+
| _ -> begin
+
default
+
end
+
end
+
| _ -> begin
+
default
+
end
+
+
let get_param_string json name default =
+
match json with
+
| `Assoc fields -> begin
+
match List.assoc_opt name fields with
+
| Some (`String s) -> begin
+
s
+
end
+
| _ -> begin
+
default
+
end
+
end
+
| _ -> begin
+
default
+
end
+
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Multimodal Example"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
Printf.fprintf stderr "MultimodalServer is starting up!\n";
+
flush stderr;
+
Log.info "MultimodalServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "MultimodalServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "MultimodalServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Define and register a multimodal tool *)
+
let _ = add_tool server
+
~name:"generate_multimodal_response"
+
~description:"Generates a response with text, image and audio content"
+
~schema_properties:[
+
("message", "string", "The text message to include");
+
("color", "string", "Color for the image (hex format #RRGGBB)");
+
("frequency", "integer", "Frequency for the audio tone in Hz");
+
]
+
~schema_required:["message"]
+
(fun args ->
+
try
+
let message = get_param_string args "message" "Hello, multimodal world!" in
+
let color = get_param_string args "color" "#FF0000" in
+
let frequency = get_param_int args "frequency" 440 in
+
+
(* Generate image *)
+
let image_data = ImageGenerator.generate_simple_image 100 100 color in
+
let image_base64 = Base64.encode image_data in
+
+
(* Generate audio *)
+
let audio_data = AudioGenerator.generate_sine_wave
+
~frequency:(float_of_int frequency)
+
~duration:1.0
+
~sample_rate:8000
+
~amplitude:0.8 in
+
let audio_base64 = Base64.encode audio_data in
+
+
(* Create a response with text, image and audio content *)
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{
+
text = message;
+
annotations = None
+
};
+
Image ImageContent.{
+
data = image_base64;
+
mime_type = "image/png";
+
annotations = None
+
};
+
Audio AudioContent.{
+
data = audio_base64;
+
mime_type = "audio/wav";
+
annotations = None
+
}
+
];
+
is_error = false;
+
meta = None
+
}
+
with
+
| Failure msg ->
+
Log.error (Printf.sprintf "Error in multimodal tool: %s" msg);
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{
+
text = Printf.sprintf "Error: %s" msg;
+
annotations = None
+
}
+
];
+
is_error = true;
+
meta = None
+
}
+
)
+
+
(* Define and register a multimodal prompt *)
+
let _ = add_prompt server
+
~name:"multimodal-prompt"
+
~description:"A prompt that includes text, image, and audio"
+
~arguments:[
+
("message", Some "Text message to include", true);
+
("color", Some "Color for the image (hex format #RRGGBB)", false);
+
("frequency", Some "Frequency for the audio tone in Hz", false);
+
]
+
(fun args ->
+
(* Parse parameters with defaults *)
+
let message =
+
try List.assoc "message" args
+
with Not_found -> "Hello, multimodal world!"
+
in
+
+
let color =
+
try List.assoc "color" args
+
with Not_found -> "#0000FF"
+
in
+
+
let frequency =
+
try int_of_string (List.assoc "frequency" args)
+
with _ -> 440
+
in
+
+
(* Generate image *)
+
let image_data = ImageGenerator.generate_simple_image 100 100 color in
+
let image_base64 = Base64.encode image_data in
+
+
(* Generate audio *)
+
let audio_data = AudioGenerator.generate_sine_wave
+
~frequency:(float_of_int frequency)
+
~duration:1.0
+
~sample_rate:8000
+
~amplitude:0.8 in
+
let audio_base64 = Base64.encode audio_data in
+
+
(* Create a multimodal prompt *)
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content "Here's a multimodal message with text, image, and audio:"
+
};
+
Prompt.{
+
role = `User;
+
content = make_text_content message
+
};
+
Prompt.{
+
role = `User;
+
content = make_image_content image_base64 "image/png"
+
};
+
Prompt.{
+
role = `User;
+
content = make_audio_content audio_base64 "audio/wav"
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content "I've received your multimodal message with text, image, and audio."
+
}
+
]
+
)
+
+
(* Also register a resource prompt example *)
+
let _ = add_prompt server
+
~name:"resource-prompt"
+
~description:"A prompt that includes embedded resources"
+
~arguments:[
+
("resource_id", Some "ID of the resource to include", true);
+
]
+
(fun args ->
+
(* Sample resource texts *)
+
let resources = [
+
("doc1", "This is the content of document 1.");
+
("doc2", "Document 2 contains important information about OCaml.");
+
("doc3", "Document 3 explains the MCP protocol in detail.");
+
] in
+
+
(* Get the requested resource *)
+
let resource_id =
+
try List.assoc "resource_id" args
+
with Not_found -> "doc1"
+
in
+
+
(* Find the resource content *)
+
let resource_content =
+
try List.assoc resource_id resources
+
with Not_found -> Printf.sprintf "Resource '%s' not found" resource_id
+
in
+
+
(* Create a prompt with embedded resource *)
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content (Printf.sprintf "Here's the content of resource %s:" resource_id)
+
};
+
Prompt.{
+
role = `User;
+
content = make_text_resource_content (Printf.sprintf "resource://%s" resource_id) resource_content ~mime_type:"text/plain" ()
+
};
+
Prompt.{
+
role = `User;
+
content = make_text_content "Please analyze this content."
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content "I'll analyze the resource content for you."
+
}
+
]
+
)
+
+
(* Main function *)
+
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: multimodal_example [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
+
(* Configure the server with appropriate capabilities *)
+
let server = configure_server server ~with_tools:true ~with_resources:false ~with_prompts:true () in
+
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server;
+13 -2
bin/resource_template_example.ml
···
open Mcp
open Mcp_sdk
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
(* Instead of printing directly to stdout which messes up the JSON-RPC protocol,
use the logging system which sends output to stderr *)
Log.info "Starting ResourceTemplateServer...";
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
-
(* Run the server *)
-
run_server server
···
open Mcp
open Mcp_sdk
+
open Mcp_server
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
(* Main function *)
let () =
+
(* Parse command line arguments *)
+
let transport_type = ref Stdio in
+
let args = [
+
("--http", Arg.Unit (fun () -> transport_type := Http),
+
"Start server with HTTP transport (default is stdio)");
+
] in
+
let usage_msg = "Usage: resource_template_example [--http]" in
+
Arg.parse args (fun _ -> ()) usage_msg;
+
(* Instead of printing directly to stdout which messes up the JSON-RPC protocol,
use the logging system which sends output to stderr *)
Log.info "Starting ResourceTemplateServer...";
···
(* Configure the server with appropriate capabilities *)
ignore (configure_server server ());
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server
+6
lib/dune
···
(libraries mcp jsonrpc unix yojson)
(modules mcp_sdk)
(flags (:standard -w -67 -w -27 -w -32)))
···
(libraries mcp jsonrpc unix yojson)
(modules mcp_sdk)
(flags (:standard -w -67 -w -27 -w -32)))
+
+
(library
+
(name mcp_server)
+
(libraries mcp mcp_sdk jsonrpc unix yojson)
+
(modules mcp_server)
+
(flags (:standard -w -67 -w -27 -w -32 -w -8 -w -11 -w -33)))
+78 -7
lib/mcp_sdk.ml
···
let log level msg =
Printf.eprintf "[%s] %s\n" (string_of_level level) msg;
-
flush stderr;
-
Printf.printf "[%s] %s\n" (string_of_level level) msg;
-
flush stdout
let debug = log Debug
let info = log Info
···
let create_argument ~name ?description ?(required=false) () =
{ name; description; required }
let to_json prompt =
let assoc = [
("name", `String prompt.name);
···
let make_text_content text =
Text (TextContent.{ text; annotations = None })
let make_tool_schema properties required =
let props = List.map (fun (name, schema_type, description) ->
(name, `Assoc [
···
let set_shutdown_hook server hook =
server.shutdown_hook <- Some hook
-
(* Run the server *)
let run_server server =
(* Setup *)
Printexc.record_backtrace true;
-
set_binary_mode_out stdout false;
-
Log.info (Printf.sprintf "%s server started" server.name);
Log.debug (Printf.sprintf "Protocol version: %s" server.protocol_version);
Log.debug (Printf.sprintf "Server info: %s v%s" server.name server.version);
···
| Some hook -> hook ()
| None -> ());
-
Log.info "Server initialized and ready."
···
let log level msg =
Printf.eprintf "[%s] %s\n" (string_of_level level) msg;
+
flush stderr
let debug = log Debug
let info = log Info
···
let create_argument ~name ?description ?(required=false) () =
{ name; description; required }
+
let yojson_of_message { role; content } =
+
`Assoc [
+
("role", Role.yojson_of_t role);
+
("content", yojson_of_content content);
+
]
+
+
(* This function must match the structure expected by the PromptMessage module in mcp.ml *)
+
let message_of_yojson json =
+
match json with
+
| `Assoc fields -> begin
+
let role = match List.assoc_opt "role" fields with
+
| Some json -> begin
+
Role.t_of_yojson json
+
end
+
| None -> begin
+
raise (Json.Of_json ("Missing role field", `Assoc fields))
+
end
+
in
+
let content = match List.assoc_opt "content" fields with
+
| Some json -> begin
+
content_of_yojson json
+
end
+
| None -> begin
+
raise (Json.Of_json ("Missing content field", `Assoc fields))
+
end
+
in
+
{ role; content }
+
end
+
| j -> begin
+
raise (Json.Of_json ("Expected object for PromptMessage", j))
+
end
+
let to_json prompt =
let assoc = [
("name", `String prompt.name);
···
let make_text_content text =
Text (TextContent.{ text; annotations = None })
+
let make_text_content_with_annotations text annotations =
+
Text (TextContent.{ text; annotations = Some annotations })
+
+
let make_image_content data mime_type =
+
Image (ImageContent.{ data; mime_type; annotations = None })
+
+
let make_image_content_with_annotations data mime_type annotations =
+
Image (ImageContent.{ data; mime_type; annotations = Some annotations })
+
+
let make_audio_content data mime_type =
+
Audio (AudioContent.{ data; mime_type; annotations = None })
+
+
let make_audio_content_with_annotations data mime_type annotations =
+
Audio (AudioContent.{ data; mime_type; annotations = Some annotations })
+
+
let make_text_resource_content uri text ?mime_type () =
+
Resource (EmbeddedResource.{
+
resource = `Text TextResourceContents.{ uri; text; mime_type };
+
annotations = None
+
})
+
+
let make_blob_resource_content uri blob ?mime_type () =
+
Resource (EmbeddedResource.{
+
resource = `Blob BlobResourceContents.{ uri; blob; mime_type };
+
annotations = None
+
})
+
let make_tool_schema properties required =
let props = List.map (fun (name, schema_type, description) ->
(name, `Assoc [
···
let set_shutdown_hook server hook =
server.shutdown_hook <- Some hook
+
(* Transport type for server *)
+
type transport_type =
+
| Stdio (* Read/write to stdin/stdout *)
+
| Http (* HTTP server - to be implemented *)
+
+
(* Run server with stdio transport *)
let run_server server =
(* Setup *)
Printexc.record_backtrace true;
+
Log.info (Printf.sprintf "%s server starting" server.name);
Log.debug (Printf.sprintf "Protocol version: %s" server.protocol_version);
Log.debug (Printf.sprintf "Server info: %s v%s" server.name server.version);
···
| Some hook -> hook ()
| None -> ());
+
(* This function will be replaced by a full implementation in the mcp_server module *)
+
Log.info "Server initialized and ready."
+
+
(* Placeholder for running server with different transports *)
+
let run_server_with_transport server transport =
+
match transport with
+
| Http ->
+
Log.info "HTTP server not implemented in this version, using stdio instead";
+
run_server server
+
| Stdio ->
+
run_server server
+18 -1
lib/mcp_sdk.mli
···
val create : name:string -> ?description:string -> ?arguments:argument list -> handler:handler -> unit -> t
val create_argument : name:string -> ?description:string -> ?required:bool -> unit -> argument
val to_json : t -> Json.t
end
···
(** Set shutdown hook *)
val set_shutdown_hook : server -> (unit -> unit) -> unit
-
(** Run the server *)
val run_server : server -> unit
(** Helper functions for creating common objects *)
val make_text_content : string -> content
val make_tool_schema : (string * string * string) list -> string list -> Json.t
···
val create : name:string -> ?description:string -> ?arguments:argument list -> handler:handler -> unit -> t
val create_argument : name:string -> ?description:string -> ?required:bool -> unit -> argument
+
val yojson_of_message : message -> Json.t
+
val message_of_yojson : Json.t -> message
val to_json : t -> Json.t
end
···
(** Set shutdown hook *)
val set_shutdown_hook : server -> (unit -> unit) -> unit
+
(** Run the server using stdio transport (legacy method) *)
val run_server : server -> unit
+
(** Transport type for the server *)
+
type transport_type =
+
| Stdio (** Read/write to stdin/stdout *)
+
| Http (** HTTP server - to be implemented *)
+
+
(** Create and start a server with the specified transport *)
+
val run_server_with_transport : server -> transport_type -> unit
+
(** Helper functions for creating common objects *)
val make_text_content : string -> content
+
val make_text_content_with_annotations : string -> Annotated.annotation -> content
+
val make_image_content : string -> string -> content
+
val make_image_content_with_annotations : string -> string -> Annotated.annotation -> content
+
val make_audio_content : string -> string -> content
+
val make_audio_content_with_annotations : string -> string -> Annotated.annotation -> content
+
val make_text_resource_content : string -> string -> ?mime_type:string -> unit -> content
+
val make_blob_resource_content : string -> string -> ?mime_type:string -> unit -> content
val make_tool_schema : (string * string * string) list -> string list -> Json.t
+533
lib/mcp_server.ml
···
···
+
open Mcp
+
open Mcp_sdk
+
+
(* MCP Server module for handling JSON-RPC communication *)
+
+
(** Server types *)
+
type transport_type =
+
| Stdio (* Read/write to stdin/stdout *)
+
| Http (* HTTP server - to be implemented *)
+
+
type t = {
+
server: Mcp_sdk.server;
+
transport: transport_type;
+
mutable running: bool;
+
}
+
+
(** Process a single message *)
+
let process_message server message =
+
try
+
Log.debug "Parsing message as JSONRPC message...";
+
match JSONRPCMessage.t_of_yojson message with
+
| JSONRPCMessage.Request req -> begin
+
Log.debug (Printf.sprintf "Received request with method: %s" req.method_);
+
match req.method_ with
+
| "initialize" -> begin
+
Log.debug "Processing initialize request";
+
let result = match req.params with
+
| Some params -> begin
+
Log.debug "Parsing initialize request params...";
+
let req_params = Initialize.Request.t_of_yojson params in
+
Log.debug (Printf.sprintf "Client info: %s v%s"
+
req_params.client_info.name
+
req_params.client_info.version);
+
Log.debug (Printf.sprintf "Client protocol version: %s" req_params.protocol_version);
+
+
(* Check protocol version compatibility *)
+
if req_params.protocol_version <> server.protocol_version then begin
+
Log.debug (Printf.sprintf "Protocol version mismatch: client=%s server=%s"
+
req_params.protocol_version server.protocol_version);
+
end;
+
+
Initialize.Result.create
+
~capabilities:server.capabilities
+
~server_info:Implementation.{ name = server.name; version = server.version }
+
~protocol_version:server.protocol_version
+
?instructions:(Some "MCP Server") (* TODO: Allow customization *)
+
()
+
end
+
| None -> begin
+
Log.error "Missing params for initialize request";
+
Initialize.Result.create
+
~capabilities:server.capabilities
+
~server_info:Implementation.{ name = server.name; version = server.version }
+
~protocol_version:server.protocol_version
+
()
+
end
+
in
+
Some (create_response ~id:req.id ~result:(Initialize.Result.yojson_of_t result))
+
end
+
+
| "tools/list" -> begin
+
Log.debug "Processing tools/list request";
+
let tools_json = List.map Mcp_sdk.Tool.to_json server.tools in
+
let result = `Assoc [("tools", `List tools_json)] in
+
Some (create_response ~id:req.id ~result)
+
end
+
+
| "tools/call" -> begin
+
Log.debug "Processing tools/call request";
+
match req.params with
+
| Some (`Assoc params) -> begin
+
let name = match List.assoc_opt "name" params with
+
| Some (`String name) -> begin
+
Log.debug (Printf.sprintf "Tool name: %s" name);
+
name
+
end
+
| _ -> begin
+
Log.error "Missing or invalid 'name' parameter in tool call";
+
failwith "Missing or invalid 'name' parameter"
+
end
+
in
+
let args = match List.assoc_opt "arguments" params with
+
| Some args -> begin
+
Log.debug (Printf.sprintf "Tool arguments: %s" (Yojson.Safe.to_string args));
+
args
+
end
+
| _ -> begin
+
Log.debug "No arguments provided for tool call, using empty object";
+
`Assoc [] (* Empty arguments is valid *)
+
end
+
in
+
let progress_token = req.progress_token in
+
+
(* Find the tool *)
+
let tool_opt = List.find_opt (fun t -> t.Mcp_sdk.Tool.name = name) server.tools in
+
match tool_opt with
+
| Some tool -> begin
+
Log.debug (Printf.sprintf "Found tool: %s" name);
+
let ctx = Mcp_sdk.Context.create
+
?request_id:(Some req.id)
+
~lifespan_context:server.lifespan_context
+
()
+
in
+
ctx.progress_token <- progress_token;
+
+
(* Call the handler *)
+
let result = match tool.handler ctx args with
+
| Ok json -> begin
+
`Assoc [
+
("content", `List [Mcp.yojson_of_content (Text (TextContent.{
+
text = Yojson.Safe.to_string json;
+
annotations = None
+
}))]);
+
("isError", `Bool false)
+
]
+
end
+
| Error err -> begin
+
`Assoc [
+
("content", `List [Mcp.yojson_of_content (Text (TextContent.{
+
text = err;
+
annotations = None
+
}))]);
+
("isError", `Bool true)
+
]
+
end
+
in
+
Some (create_response ~id:req.id ~result)
+
end
+
| None -> begin
+
Log.error (Printf.sprintf "Tool not found: %s" name);
+
let error_content = TextContent.{
+
text = Printf.sprintf "Unknown tool: %s" name;
+
annotations = None
+
} in
+
let result = `Assoc [
+
("content", `List [Mcp.yojson_of_content (Text error_content)]);
+
("isError", `Bool true)
+
] in
+
Some (create_response ~id:req.id ~result)
+
end
+
end
+
| _ -> begin
+
Log.error "Invalid params format for tools/call";
+
Some (create_error ~id:req.id ~code:ErrorCode.invalid_params ~message:"Invalid params for tools/call" ())
+
end
+
end
+
+
| "resources/list" -> begin
+
Log.debug "Processing resources/list request";
+
if server.resources <> [] then begin
+
let resources_json = List.map Mcp_sdk.Resource.to_json server.resources in
+
let result = `Assoc [("resources", `List resources_json)] in
+
Some (create_response ~id:req.id ~result)
+
end else begin
+
Some (create_error ~id:req.id ~code:ErrorCode.method_not_found ~message:"Resources not supported" ())
+
end
+
end
+
+
| "prompts/list" -> begin
+
Log.debug "Processing prompts/list request";
+
if server.prompts <> [] then begin
+
let prompts_json = List.map Mcp_sdk.Prompt.to_json server.prompts in
+
let result = `Assoc [("prompts", `List prompts_json)] in
+
Some (create_response ~id:req.id ~result)
+
end else begin
+
Some (create_error ~id:req.id ~code:ErrorCode.method_not_found ~message:"Prompts not supported" ())
+
end
+
end
+
+
| "prompts/get" -> begin
+
Log.debug "Processing prompts/get request";
+
if server.prompts <> [] then begin
+
match req.params with
+
| Some (`Assoc params) -> begin
+
(* Extract prompt name *)
+
let name = match List.assoc_opt "name" params with
+
| Some (`String name) -> begin
+
Log.debug (Printf.sprintf "Prompt name: %s" name);
+
name
+
end
+
| _ -> begin
+
Log.error "Missing or invalid 'name' parameter in prompt request";
+
failwith "Missing or invalid 'name' parameter"
+
end
+
in
+
+
(* Extract arguments if any *)
+
let arguments = match List.assoc_opt "arguments" params with
+
| Some (`Assoc args) -> begin
+
Log.debug (Printf.sprintf "Prompt arguments: %s" (Yojson.Safe.to_string (`Assoc args)));
+
List.map (fun (k, v) ->
+
match v with
+
| `String s -> begin (k, s) end
+
| _ -> begin (k, Yojson.Safe.to_string v) end
+
) args
+
end
+
| _ -> begin
+
[]
+
end
+
in
+
+
(* Find the prompt *)
+
let prompt_opt = List.find_opt (fun p -> p.Mcp_sdk.Prompt.name = name) server.prompts in
+
match prompt_opt with
+
| Some prompt -> begin
+
Log.debug (Printf.sprintf "Found prompt: %s" name);
+
let ctx = Mcp_sdk.Context.create
+
?request_id:(Some req.id)
+
~lifespan_context:server.lifespan_context
+
()
+
in
+
+
(* Call the prompt handler *)
+
match prompt.handler ctx arguments with
+
| Ok messages -> begin
+
Log.debug (Printf.sprintf "Prompt handler returned %d messages" (List.length messages));
+
+
(* Important: We need to directly use yojson_of_message which preserves MIME types *)
+
let messages_json = List.map Prompt.yojson_of_message messages in
+
+
(* Debug output *)
+
Log.debug (Printf.sprintf "Messages JSON: %s" (Yojson.Safe.to_string (`List messages_json)));
+
+
(* Verify one message if available to check structure *)
+
if List.length messages > 0 then begin
+
let first_msg = List.hd messages in
+
let content_debug = match first_msg.content with
+
| Text t -> begin
+
Printf.sprintf "Text content: %s" t.text
+
end
+
| Image i -> begin
+
Printf.sprintf "Image content (mime: %s)" i.mime_type
+
end
+
| Audio a -> begin
+
Printf.sprintf "Audio content (mime: %s)" a.mime_type
+
end
+
| Resource r -> begin
+
"Resource content"
+
end
+
in
+
Log.debug (Printf.sprintf "First message content type: %s" content_debug);
+
end;
+
+
let result = `Assoc [
+
("messages", `List messages_json);
+
("description", match prompt.description with
+
| Some d -> begin `String d end
+
| None -> begin `Null end)
+
] in
+
Some (create_response ~id:req.id ~result)
+
end
+
| Error err -> begin
+
Log.error (Printf.sprintf "Error processing prompt: %s" err);
+
Some (create_error ~id:req.id ~code:ErrorCode.internal_error ~message:err ())
+
end
+
end
+
| None -> begin
+
Log.error (Printf.sprintf "Prompt not found: %s" name);
+
Some (create_error ~id:req.id ~code:ErrorCode.invalid_params ~message:(Printf.sprintf "Prompt not found: %s" name) ())
+
end
+
end
+
| _ -> begin
+
Log.error "Invalid params format for prompts/get";
+
Some (create_error ~id:req.id ~code:ErrorCode.invalid_params ~message:"Invalid params format" ())
+
end
+
end else begin
+
Some (create_error ~id:req.id ~code:ErrorCode.method_not_found ~message:"Prompts not supported" ())
+
end
+
end
+
+
| "ping" -> begin
+
Log.debug "Processing ping request";
+
Some (create_response ~id:req.id ~result:(`Assoc []))
+
end
+
+
| _ -> begin
+
Log.error (Printf.sprintf "Unknown method received: %s" req.method_);
+
Some (create_error ~id:req.id ~code:ErrorCode.method_not_found ~message:("Method not found: " ^ req.method_) ())
+
end
+
end
+
+
| JSONRPCMessage.Notification notif -> begin
+
Log.debug (Printf.sprintf "Received notification with method: %s" notif.method_);
+
match notif.method_ with
+
| "notifications/initialized" -> begin
+
Log.debug "Client initialization complete - Server is now ready to receive requests";
+
None
+
end
+
| _ -> begin
+
Log.debug (Printf.sprintf "Ignoring notification: %s" notif.method_);
+
None
+
end
+
end
+
+
| JSONRPCMessage.Response _ -> begin
+
Log.error "Unexpected response message received";
+
None
+
end
+
+
| JSONRPCMessage.Error _ -> begin
+
Log.error "Unexpected error message received";
+
None
+
end
+
with
+
| Failure msg -> begin
+
Log.error (Printf.sprintf "JSON error in message processing: %s" msg);
+
None
+
end
+
| exc -> begin
+
Log.error (Printf.sprintf "Exception during message processing: %s" (Printexc.to_string exc));
+
Log.error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
None
+
end
+
+
(** Read a single message from stdin *)
+
let read_stdio_message () =
+
try
+
Log.debug "Reading line from stdin...";
+
let line = read_line () in
+
if line = "" then begin
+
Log.debug "Empty line received, ignoring";
+
None
+
end else begin
+
Log.debug (Printf.sprintf "Raw input: %s" (String.sub line 0 (min 100 (String.length line))));
+
try
+
let json = Yojson.Safe.from_string line in
+
Log.debug "Successfully parsed JSON";
+
Some json
+
with
+
| Yojson.Json_error msg -> begin
+
Log.error (Printf.sprintf "Error parsing JSON: %s" msg);
+
Log.error (Printf.sprintf "Input was: %s" (String.sub line 0 (min 100 (String.length line))));
+
None
+
end
+
end
+
with
+
| End_of_file -> begin
+
Log.debug "End of file received on stdin";
+
None
+
end
+
| Sys_error msg -> begin
+
Log.error (Printf.sprintf "System error while reading: %s" msg);
+
None
+
end
+
| exc -> begin
+
Log.error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exc));
+
None
+
end
+
+
(** Run stdio server with enhanced error handling *)
+
let rec run_stdio_server mcp_server =
+
try begin
+
if not mcp_server.running then begin
+
Log.debug "Server stopped";
+
()
+
end else begin
+
match read_stdio_message () with
+
| Some json -> begin
+
Log.debug "Processing message...";
+
try begin
+
match process_message mcp_server.server json with
+
| Some response -> begin
+
let response_json = JSONRPCMessage.yojson_of_t response in
+
let response_str = Yojson.Safe.to_string response_json in
+
Log.debug (Printf.sprintf "Sending response: %s"
+
(String.sub response_str 0 (min 100 (String.length response_str))));
+
Printf.printf "%s\n" response_str;
+
flush stdout;
+
(* Give client time to process *)
+
Unix.sleepf 0.01;
+
end
+
| None -> begin
+
Log.debug "No response needed"
+
end
+
end with
+
| exc -> begin
+
Log.error (Printf.sprintf "ERROR in message processing: %s" (Printexc.to_string exc));
+
Log.error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
(* Try to extract ID and send an error response *)
+
try begin
+
let id_opt = match Yojson.Safe.Util.member "id" json with
+
| `Int i -> Some (`Int i)
+
| `String s -> Some (`String s)
+
| _ -> None
+
in
+
match id_opt with
+
| Some id -> begin
+
let error_resp = create_error ~id ~code:ErrorCode.internal_error ~message:(Printexc.to_string exc) () in
+
let error_json = JSONRPCMessage.yojson_of_t error_resp in
+
let error_str = Yojson.Safe.to_string error_json in
+
Printf.printf "%s\n" error_str;
+
flush stdout;
+
end
+
| None -> begin
+
Log.error "Could not extract request ID to send error response"
+
end
+
end with
+
| e -> begin
+
Log.error (Printf.sprintf "Failed to send error response: %s" (Printexc.to_string e))
+
end
+
end;
+
run_stdio_server mcp_server
+
end
+
| None -> begin
+
if mcp_server.running then begin
+
(* No message received, but server is still running *)
+
Unix.sleepf 0.1; (* Small sleep to prevent CPU spinning *)
+
run_stdio_server mcp_server
+
end else begin
+
Log.debug "Server stopped during message processing"
+
end
+
end
+
end
+
end with
+
| exc -> begin
+
Log.error (Printf.sprintf "FATAL ERROR in server main loop: %s" (Printexc.to_string exc));
+
Log.error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
(* Try to continue anyway *)
+
if mcp_server.running then begin
+
Unix.sleepf 0.1;
+
run_stdio_server mcp_server
+
end
+
end
+
+
(** Create an MCP server *)
+
let create ~server ~transport () =
+
{ server; transport; running = false }
+
+
(** HTTP server placeholder (to be fully implemented) *)
+
let run_http_server mcp_server port =
+
Log.info (Printf.sprintf "%s HTTP server starting on port %d" mcp_server.server.name port);
+
Log.info "HTTP transport is a placeholder and not fully implemented yet";
+
+
(* This would be where we'd set up cohttp server *)
+
(*
+
let callback _conn req body =
+
let uri = req |> Cohttp.Request.uri in
+
let meth = req |> Cohttp.Request.meth |> Cohttp.Code.string_of_method in
+
+
(* Handle only POST /jsonrpc endpoint *)
+
match (meth, Uri.path uri) with
+
| "POST", "/jsonrpc" ->
+
(* Read the body *)
+
Cohttp_lwt.Body.to_string body >>= fun body_str ->
+
+
(* Parse JSON *)
+
let json = try Some (Yojson.Safe.from_string body_str) with _ -> None in
+
match json with
+
| Some json_msg ->
+
(* Process the message *)
+
let response_opt = process_message mcp_server.server json_msg in
+
(match response_opt with
+
| Some response ->
+
let response_json = JSONRPCMessage.yojson_of_t response in
+
let response_str = Yojson.Safe.to_string response_json in
+
Cohttp_lwt_unix.Server.respond_string
+
~status:`OK
+
~body:response_str
+
~headers:(Cohttp.Header.init_with "Content-Type" "application/json")
+
()
+
| None ->
+
Cohttp_lwt_unix.Server.respond_string
+
~status:`OK
+
~body:"{}"
+
~headers:(Cohttp.Header.init_with "Content-Type" "application/json")
+
())
+
| None ->
+
Cohttp_lwt_unix.Server.respond_string
+
~status:`Bad_request
+
~body:"{\"error\":\"Invalid JSON\"}"
+
~headers:(Cohttp.Header.init_with "Content-Type" "application/json")
+
()
+
| _ ->
+
(* Return 404 for any other routes *)
+
Cohttp_lwt_unix.Server.respond_string
+
~status:`Not_found
+
~body:"Not found"
+
()
+
in
+
+
(* Create and start the server *)
+
let server = Cohttp_lwt_unix.Server.create
+
~mode:(`TCP (`Port port))
+
(Cohttp_lwt_unix.Server.make ~callback ())
+
in
+
+
(* Run the server *)
+
Lwt_main.run server
+
*)
+
+
(* For now, just wait until the server is stopped *)
+
while mcp_server.running do
+
Unix.sleep 1
+
done
+
+
(** Start the server based on transport type *)
+
let start server =
+
server.running <- true;
+
+
(* Run startup hook if provided *)
+
(match server.server.startup_hook with
+
| Some hook -> begin hook () end
+
| None -> begin () end);
+
+
(* Install signal handler *)
+
Sys.(set_signal sigint (Signal_handle (fun _ ->
+
Log.debug "Received interrupt signal, stopping server...";
+
server.running <- false
+
)));
+
+
match server.transport with
+
| Stdio -> begin
+
(* Setup stdout and stderr *)
+
set_binary_mode_out stdout false;
+
Log.info (Printf.sprintf "%s server started with stdio transport" server.server.name);
+
+
(* Run the server loop *)
+
run_stdio_server server
+
end
+
| Http -> begin
+
(* HTTP server placeholder *)
+
run_http_server server 8080
+
end
+
+
(** Stop the server *)
+
let stop server =
+
Log.info "Stopping server...";
+
server.running <- false;
+
+
(* Run shutdown hook if provided *)
+
match server.server.shutdown_hook with
+
| Some hook -> begin hook () end
+
| None -> begin () end
+54
lib/mcp_server.mli
···
···
+
(** MCP Server module - full implementation *)
+
+
(** Transport type for server *)
+
type transport_type =
+
| Stdio (** Read/write to stdin/stdout *)
+
| Http (** HTTP server - to be implemented *)
+
+
(** Server type *)
+
type t = {
+
server: Mcp_sdk.server;
+
transport: transport_type;
+
mutable running: bool;
+
}
+
+
(** Create an MCP server
+
@param server The Mcp_sdk server to use
+
@param transport The transport type to use
+
*)
+
val create : server:Mcp_sdk.server -> transport:transport_type -> unit -> t
+
+
(** Start the server
+
This function will block until the server is stopped.
+
@param server The server to start
+
*)
+
val start : t -> unit
+
+
(** Stop the server
+
This will set the running flag to false and invoke the shutdown hook.
+
@param server The server to stop
+
*)
+
val stop : t -> unit
+
+
(** Process a single message
+
@param server The Mcp_sdk server to use
+
@param message The JSON message to process
+
@return An optional response message
+
*)
+
val process_message : Mcp_sdk.server -> Yojson.Safe.t -> Mcp.JSONRPCMessage.t option
+
+
(** Run stdio server implementation
+
@param mcp_server The mcp_server to run
+
*)
+
val run_stdio_server : t -> unit
+
+
(** Read a message from stdio
+
@return An optional JSON message
+
*)
+
val read_stdio_message : unit -> Yojson.Safe.t option
+
+
(** Run HTTP server implementation (placeholder)
+
@param mcp_server The mcp_server to run
+
@param port The port to listen on
+
*)
+
val run_http_server : t -> int -> unit