Model Context Protocol in OCaml

Compare changes

Choose any two refs to compare.

+330
bin/audio_example.ml
···
+
open Mcp
+
open Mcp_sdk
+
open Mcp_server
+
+
(* WAV file format helper module *)
+
module Wav = struct
+
(* Simple WAV file generation for a 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_of_int (file_size land 0xff));
+
Buffer.add_char buffer (char_of_int ((file_size lsr 8) land 0xff));
+
Buffer.add_char buffer (char_of_int ((file_size lsr 16) land 0xff));
+
Buffer.add_char buffer (char_of_int ((file_size lsr 24) land 0xff));
+
Buffer.add_string buffer "WAVE";
+
+
(* "fmt " sub-chunk *)
+
Buffer.add_string buffer "fmt ";
+
Buffer.add_char buffer (char_of_int 16); (* Sub-chunk size (16 for PCM) *)
+
Buffer.add_char buffer (char_of_int 0);
+
Buffer.add_char buffer (char_of_int 0);
+
Buffer.add_char buffer (char_of_int 0);
+
Buffer.add_char buffer (char_of_int 1); (* Audio format (1 for PCM) *)
+
Buffer.add_char buffer (char_of_int 0);
+
Buffer.add_char buffer (char_of_int num_channels); (* Number of channels *)
+
Buffer.add_char buffer (char_of_int 0);
+
+
(* Sample rate *)
+
Buffer.add_char buffer (char_of_int (sample_rate land 0xff));
+
Buffer.add_char buffer (char_of_int ((sample_rate lsr 8) land 0xff));
+
Buffer.add_char buffer (char_of_int ((sample_rate lsr 16) land 0xff));
+
Buffer.add_char buffer (char_of_int ((sample_rate lsr 24) land 0xff));
+
+
(* Byte rate *)
+
Buffer.add_char buffer (char_of_int (byte_rate land 0xff));
+
Buffer.add_char buffer (char_of_int ((byte_rate lsr 8) land 0xff));
+
Buffer.add_char buffer (char_of_int ((byte_rate lsr 16) land 0xff));
+
Buffer.add_char buffer (char_of_int ((byte_rate lsr 24) land 0xff));
+
+
(* Block align *)
+
Buffer.add_char buffer (char_of_int block_align);
+
Buffer.add_char buffer (char_of_int 0);
+
+
(* Bits per sample *)
+
Buffer.add_char buffer (char_of_int bits_per_sample);
+
Buffer.add_char buffer (char_of_int 0);
+
+
(* "data" sub-chunk *)
+
Buffer.add_string buffer "data";
+
Buffer.add_char buffer (char_of_int (data_size land 0xff));
+
Buffer.add_char buffer (char_of_int ((data_size lsr 8) land 0xff));
+
Buffer.add_char buffer (char_of_int ((data_size lsr 16) land 0xff));
+
Buffer.add_char buffer (char_of_int ((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_of_int (value land 0xff));
+
Buffer.add_char buffer (char_of_int ((value lsr 8) land 0xff));
+
done;
+
+
Buffer.contents buffer
+
+
(* Encode binary data as base64 *)
+
let base64_encode data =
+
let buffer = Buffer.create (4 * (String.length data + 2) / 3) in
+
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" in
+
+
let encode_block i bytes =
+
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 (String.get alphabet ((n lsr 18) land 63));
+
Buffer.add_char buffer (String.get alphabet ((n lsr 12) land 63));
+
+
if i * 3 + 1 < String.length bytes then
+
Buffer.add_char buffer (String.get alphabet ((n lsr 6) land 63))
+
else
+
Buffer.add_char buffer '=';
+
+
if i * 3 + 2 < String.length bytes then
+
Buffer.add_char buffer (String.get alphabet (n land 63))
+
else
+
Buffer.add_char buffer '=';
+
in
+
+
for i = 0 to (String.length data + 2) / 3 - 1 do
+
encode_block i data
+
done;
+
+
Buffer.contents buffer
+
end
+
+
(* Helper for extracting string value from JSON *)
+
let get_string_param json name =
+
match json with
+
| `Assoc fields ->
+
(match List.assoc_opt name fields with
+
| Some (`String value) -> value
+
| _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name)))
+
| _ -> raise (Failure "Expected JSON object")
+
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Audio Example"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
+
Printf.fprintf stderr "AudioExampleServer is starting up!\n";
+
flush stderr;
+
Log.info "AudioExampleServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "AudioExampleServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "AudioExampleServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Helper to create audio content *)
+
let make_audio_content data mime_type =
+
let audio_content = AudioContent.{
+
data;
+
mime_type;
+
annotations = None;
+
} in
+
Audio audio_content
+
+
(* Define and register an audio tool *)
+
let _ = add_tool server
+
~name:"generate_audio_description"
+
~description:"Generates a description with an audio sample"
+
~schema_properties:[
+
("text", "string", "The text to describe with audio");
+
("frequency", "number", "The frequency in Hz for the tone (optional)");
+
("duration", "number", "The duration in seconds for the tone (optional)");
+
("amplitude", "number", "The amplitude (0.0-1.0) for the tone (optional)");
+
]
+
~schema_required:["text"]
+
(fun args ->
+
try
+
let text = get_string_param args "text" in
+
+
(* Parse parameters with defaults *)
+
let frequency =
+
try
+
match List.assoc_opt "frequency" (match args with `Assoc l -> l | _ -> []) with
+
| Some (`Int f) -> float_of_int f
+
| Some (`Float f) -> f
+
| _ -> 440.0 (* Default to A440 *)
+
with _ -> 440.0
+
in
+
+
let duration =
+
try
+
match List.assoc_opt "duration" (match args with `Assoc l -> l | _ -> []) with
+
| Some (`Int d) -> float_of_int d
+
| Some (`Float d) -> d
+
| _ -> 2.0 (* Default to 2 seconds *)
+
with _ -> 2.0
+
in
+
+
let amplitude =
+
try
+
match List.assoc_opt "amplitude" (match args with `Assoc l -> l | _ -> []) with
+
| Some (`Int a) -> float_of_int a
+
| Some (`Float a) -> a
+
| _ -> 0.8 (* Default to 80% amplitude *)
+
with _ -> 0.8
+
in
+
+
(* Generate WAV file for the tone *)
+
let sample_rate = 44100 in (* CD quality *)
+
let wav_data = Wav.generate_sine_wave
+
~frequency
+
~duration
+
~sample_rate
+
~amplitude
+
in
+
+
(* Encode WAV data as base64 *)
+
let base64_audio = Wav.base64_encode wav_data in
+
+
Log.info (Printf.sprintf "Generated %d Hz tone for %.1f seconds (%.1f KB)"
+
(int_of_float frequency) duration
+
(float_of_int (String.length wav_data) /. 1024.0));
+
+
(* Create a response with both text and audio content *)
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{
+
text = Printf.sprintf "Description: %s (with %.1f Hz tone for %.1f seconds)"
+
text frequency duration;
+
annotations = None
+
};
+
Audio AudioContent.{
+
data = base64_audio;
+
mime_type = "audio/wav";
+
annotations = None
+
}
+
];
+
is_error = false;
+
meta = None
+
}
+
with
+
| Failure msg ->
+
Log.error (Printf.sprintf "Error in audio 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 prompt example with audio *)
+
let _ = add_prompt server
+
~name:"audio-description-prompt"
+
~description:"A prompt with audio and text content"
+
~arguments:[
+
("description", Some "Text description to accompany the audio", true);
+
("frequency", Some "Frequency in Hz for the audio tone", false);
+
("duration", Some "Duration in seconds for the audio tone", false);
+
]
+
(fun args ->
+
let description =
+
try List.assoc "description" args
+
with Not_found -> "No description provided"
+
in
+
+
(* Parse frequency with default *)
+
let frequency =
+
try float_of_string (List.assoc "frequency" args)
+
with _ -> 440.0 (* Default to A440 *)
+
in
+
+
(* Parse duration with default *)
+
let duration =
+
try float_of_string (List.assoc "duration" args)
+
with _ -> 3.0 (* Default to 3 seconds *)
+
in
+
+
(* Generate WAV data *)
+
let sample_rate = 44100 in
+
let wav_data = Wav.generate_sine_wave
+
~frequency
+
~duration
+
~sample_rate
+
~amplitude:0.8
+
in
+
+
(* Encode WAV data as base64 *)
+
let base64_audio = Wav.base64_encode wav_data in
+
+
Log.info (Printf.sprintf "Generated %.1f Hz tone for prompt (%.1f seconds, %.1f KB)"
+
frequency duration
+
(float_of_int (String.length wav_data) /. 1024.0));
+
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content "Here's a sound sample with description:"
+
};
+
Prompt.{
+
role = `User;
+
content = make_audio_content base64_audio "audio/wav"
+
};
+
Prompt.{
+
role = `User;
+
content = make_text_content (Printf.sprintf "%s (%.1f Hz tone for %.1f seconds)"
+
description frequency duration)
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content "I've received your audio file and description."
+
}
+
]
+
)
+
+
(* 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;
+
Log.info "Starting AudioExampleServer...";
+
+
(* 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
+52 -41
bin/capitalize_sdk.ml
···
open Mcp
open Mcp_sdk
-
-
(* Create the server module *)
-
module CapitalizeServer = MakeServer(struct
-
let name = "OCaml MCP Capitalizer"
-
let version = Some "0.1.0"
-
end)
+
open Mcp_server
(* Helper for extracting string value from JSON *)
let get_string_param json name =
···
| _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name)))
| _ -> raise (Failure "Expected JSON object")
-
(* Define a capitalize tool *)
-
let _ = CapitalizeServer.tool
-
~name:(Some "capitalize")
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Capitalizer"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
+
Printf.fprintf stderr "CapitalizeServer is starting up!\n";
+
flush stderr;
+
Log.info "CapitalizeServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "CapitalizeServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "CapitalizeServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Define and register a capitalize tool *)
+
let _ = add_tool server
+
~name:"capitalize"
~description:"Capitalizes the provided text"
~schema_properties:[
("text", "string", "The text to capitalize")
···
}
)
-
(* Define a resource example *)
-
let _ = CapitalizeServer.resource
-
~uri_template:(Some "greeting://{name}")
+
(* Define and register a resource example *)
+
let _ = add_resource server
+
~uri_template:"greeting://{name}"
~description:"Get a greeting for a name"
~mime_type:"text/plain"
(fun params ->
···
| _ -> "Hello, world! Welcome to the OCaml MCP server."
)
-
(* Define a prompt example *)
-
let _ = CapitalizeServer.prompt
-
~name:(Some "capitalize-prompt")
+
(* Define and register a prompt example *)
+
let _ = add_prompt server
+
~name:"capitalize-prompt"
~description:"A prompt to help with text capitalization"
~arguments:[
("text", Some "The text to be capitalized", true)
···
]
)
-
(* Define startup and shutdown hooks *)
-
let startup () =
-
Printf.printf "CapitalizeServer is starting up!\n";
-
flush stdout;
-
Log.info "CapitalizeServer is starting up!"
-
-
let shutdown () =
-
Printf.printf "CapitalizeServer is shutting down. Goodbye!\n";
-
flush stdout;
-
Log.info "CapitalizeServer is shutting down. Goodbye!"
-
(* Main function *)
let () =
-
(* Print directly to ensure we see output *)
-
Printf.printf "Starting CapitalizeServer...\n";
-
flush stdout;
-
-
(* Run the server with all our registered capabilities *)
-
let server_with_hooks = { CapitalizeServer.server with
-
Server.startup_hook = Some startup;
-
Server.shutdown_hook = Some shutdown;
-
} in
+
(* 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;
+
Log.info "Starting CapitalizeServer...";
-
(* Run the startup hook directly *)
-
(match server_with_hooks.Server.startup_hook with
-
| Some hook -> hook()
-
| None -> ());
+
(* Configure the server with appropriate capabilities *)
+
ignore (configure_server server ());
-
(* Now run the server *)
-
Server.run server_with_hooks
+
(* Create and start MCP server with the selected transport *)
+
let mcp_server = create ~server ~transport:!transport_type () in
+
start mcp_server
+193
bin/completion_example.ml
···
+
open Mcp
+
open Mcp_sdk
+
open Mcp_server
+
+
(* Helper for extracting string value from JSON *)
+
let get_string_param json name =
+
match json with
+
| `Assoc fields ->
+
(match List.assoc_opt name fields with
+
| Some (`String value) -> value
+
| _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name)))
+
| _ -> raise (Failure "Expected JSON object")
+
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Completion Example"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
+
Printf.fprintf stderr "CompletionServer is starting up!\n";
+
flush stderr;
+
Log.info "CompletionServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "CompletionServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "CompletionServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Database of programming languages and their features *)
+
let languages = [
+
("ocaml", ["functional"; "static typing"; "pattern matching"; "modules"; "type inference"]);
+
("python", ["dynamic typing"; "interpreted"; "object-oriented"; "high-level"; "scripting"]);
+
("rust", ["memory safety"; "performance"; "static typing"; "ownership"; "zero-cost abstractions"]);
+
("javascript", ["dynamic typing"; "interpreted"; "prototypes"; "single-threaded"; "event-driven"]);
+
("go", ["concurrency"; "garbage collection"; "simplicity"; "static typing"; "compiled"]);
+
]
+
+
(* Helper function to create a completion response *)
+
let create_completion values ?(has_more=false) ?(total=None) () =
+
Completion.Result.{
+
completion = {
+
values;
+
has_more = Some has_more;
+
total;
+
};
+
meta = None;
+
}
+
+
(* Define and register a tool that handles completions *)
+
let _ = add_tool server
+
~name:"complete"
+
~description:"Handles completion requests for programming languages and features"
+
~schema_properties:[
+
("argument_name", "string", "The name of the argument to complete");
+
("argument_value", "string", "The partial value to complete");
+
]
+
~schema_required:["argument_name"; "argument_value"]
+
(fun args ->
+
try
+
let argument_name = get_string_param args "argument_name" in
+
let argument_value = get_string_param args "argument_value" in
+
+
Log.info (Printf.sprintf "Completion request for %s = %s" argument_name argument_value);
+
+
(* Handle different completion requests *)
+
let result =
+
match argument_name with
+
| "language" ->
+
(* Complete programming language names *)
+
let matches =
+
List.filter
+
(fun (lang, _) ->
+
let lang_lower = String.lowercase_ascii lang in
+
let arg_lower = String.lowercase_ascii argument_value in
+
String.length lang_lower >= String.length arg_lower &&
+
String.sub lang_lower 0 (String.length arg_lower) = arg_lower)
+
languages
+
in
+
let values = List.map fst matches in
+
create_completion values ~has_more:false ~total:(Some (List.length values)) ()
+
+
| "feature" ->
+
(* Complete programming language features *)
+
let all_features =
+
List.flatten (List.map snd languages) |>
+
List.sort_uniq String.compare
+
in
+
let matches =
+
List.filter
+
(fun feature ->
+
let feature_lower = String.lowercase_ascii feature in
+
let arg_lower = String.lowercase_ascii argument_value in
+
String.length feature_lower >= String.length arg_lower &&
+
String.sub feature_lower 0 (String.length arg_lower) = arg_lower)
+
all_features
+
in
+
create_completion matches ~has_more:false ~total:(Some (List.length matches)) ()
+
+
| _ ->
+
(* Default completions for unknown arguments *)
+
create_completion ["unknown argument"] ()
+
in
+
+
(* Convert to JSON and return *)
+
TextContent.yojson_of_t TextContent.{
+
text = Yojson.Safe.to_string (Completion.Result.to_result result);
+
annotations = None
+
}
+
with
+
| Failure msg ->
+
Log.error (Printf.sprintf "Error handling completion request: %s" msg);
+
TextContent.yojson_of_t TextContent.{
+
text = Printf.sprintf "Error: %s" msg;
+
annotations = None
+
}
+
)
+
+
(* Define and register a prompt that provides programming language info *)
+
let _ = add_prompt server
+
~name:"language-info-prompt"
+
~description:"A prompt that provides information about programming languages"
+
~arguments:[
+
("language", Some "Name of the programming language", true);
+
]
+
(fun args ->
+
let language =
+
try List.assoc "language" args
+
with Not_found -> "ocaml" (* Default to OCaml *)
+
in
+
+
let features =
+
try
+
let features = List.assoc (String.lowercase_ascii language) languages in
+
String.concat ", " features
+
with Not_found -> "unknown language"
+
in
+
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content (Printf.sprintf "Tell me about the %s programming language" language)
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content (Printf.sprintf "%s is a programming language with the following features: %s" language features)
+
}
+
]
+
)
+
+
(* 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;
+
Log.info "Starting CompletionServer...";
+
+
(* Set custom capabilities to indicate support for completions *)
+
let capabilities = `Assoc [
+
("completions", `Assoc []); (* Indicate support for completions *)
+
("prompts", `Assoc [
+
("listChanged", `Bool true)
+
]);
+
("resources", `Assoc [
+
("listChanged", `Bool true);
+
("subscribe", `Bool true)
+
]);
+
("tools", `Assoc [
+
("listChanged", `Bool true)
+
])
+
] 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
+36 -2
bin/dune
···
(executable
(name server)
-
(libraries mcp yojson unix))
+
(libraries mcp yojson unix)
+
(flags (:standard -w -8-11)))
(executable
(name capitalize_sdk)
(modules capitalize_sdk)
-
(libraries mcp mcp_sdk yojson unix))
+
(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;
+186
bin/resource_template_example.ml
···
+
open Mcp
+
open Mcp_sdk
+
open Mcp_server
+
+
(* Helper for extracting string value from JSON *)
+
let get_string_param json name =
+
match json with
+
| `Assoc fields ->
+
(match List.assoc_opt name fields with
+
| Some (`String value) -> value
+
| _ -> raise (Failure (Printf.sprintf "Missing or invalid parameter: %s" name)))
+
| _ -> raise (Failure "Expected JSON object")
+
+
(* Create a server *)
+
let server = create_server
+
~name:"OCaml MCP Resource Template Example"
+
~version:"0.1.0"
+
~protocol_version:"2024-11-05"
+
()
+
+
(* Define startup and shutdown hooks *)
+
let startup () =
+
(* Use stderr for direct printing to avoid interfering with JSON-RPC protocol *)
+
Printf.fprintf stderr "ResourceTemplateServer is starting up!\n";
+
flush stderr;
+
Log.info "ResourceTemplateServer is starting up!"
+
+
let shutdown () =
+
Printf.fprintf stderr "ResourceTemplateServer is shutting down. Goodbye!\n";
+
flush stderr;
+
Log.info "ResourceTemplateServer is shutting down. Goodbye!"
+
+
(* Register the hooks *)
+
let () =
+
set_startup_hook server startup;
+
set_shutdown_hook server shutdown
+
+
(* Database of "documents" *)
+
let documents = [
+
("doc1", "This is the first document content");
+
("doc2", "This document contains information about OCaml");
+
("doc3", "MCP protocol is a standard for LLM-based applications");
+
("doc4", "Resource templates allow for parameterized resources");
+
]
+
+
(* Define and register a resource template for documents *)
+
let _ = add_resource server
+
~uri_template:"document://{id}"
+
~description:"Get a document by ID"
+
~mime_type:"text/plain"
+
(fun params ->
+
match params with
+
| [id] ->
+
begin
+
try
+
let content = List.assoc id documents in
+
content
+
with Not_found ->
+
Printf.sprintf "Error: Document '%s' not found" id
+
end
+
| _ -> "Error: Invalid document ID"
+
)
+
+
(* Define and register a list documents resource *)
+
let _ = add_resource server
+
~uri_template:"documents://list"
+
~description:"List all available documents"
+
~mime_type:"text/plain"
+
(fun _ ->
+
let doc_list =
+
String.concat "\n"
+
(List.map (fun (id, _) -> Printf.sprintf "- %s" id) documents)
+
in
+
Printf.sprintf "Available Documents:\n%s" doc_list
+
)
+
+
(* Define and register a tool that uses resource references *)
+
let _ = add_tool server
+
~name:"get_document"
+
~description:"Gets a document by ID using resource references"
+
~schema_properties:[
+
("document_id", "string", "The ID of the document to retrieve");
+
]
+
~schema_required:["document_id"]
+
(fun args ->
+
try
+
let doc_id = get_string_param args "document_id" in
+
+
(* Create a resource reference *)
+
let ref = ResourceReference.{ uri = Printf.sprintf "document://%s" doc_id } in
+
(* Convert to JSON for logging purposes *)
+
let _ = ResourceReference.yojson_of_t ref in
+
+
(* Return the reference *)
+
CallToolResult.yojson_of_t CallToolResult.{
+
content = [
+
Text TextContent.{ text = Printf.sprintf "Resource reference for document %s:" doc_id; annotations = None };
+
Resource EmbeddedResource.{
+
resource = `Text TextResourceContents.{
+
uri = Printf.sprintf "document://%s" doc_id;
+
text = (try List.assoc doc_id documents with Not_found -> "Not found");
+
mime_type = Some "text/plain"
+
};
+
annotations = None
+
}
+
];
+
is_error = false;
+
meta = None
+
}
+
with
+
| Failure msg ->
+
Log.error (Printf.sprintf "Error in get_document 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 prompt that uses resource templates *)
+
let _ = add_prompt server
+
~name:"document-prompt"
+
~description:"A prompt that references document resources"
+
~arguments:[
+
("document_id", Some "ID of the document to include in the prompt", true);
+
]
+
(fun args ->
+
let doc_id =
+
try List.assoc "document_id" args
+
with Not_found -> "doc1" (* Default to doc1 *)
+
in
+
+
let doc_text =
+
try List.assoc doc_id documents
+
with Not_found -> Printf.sprintf "Document '%s' not found" doc_id
+
in
+
+
[
+
Prompt.{
+
role = `User;
+
content = make_text_content (Printf.sprintf "Please summarize the following document (ID: %s):" doc_id)
+
};
+
Prompt.{
+
role = `User;
+
content = Resource EmbeddedResource.{
+
resource = `Text TextResourceContents.{
+
uri = Printf.sprintf "document://%s" doc_id;
+
text = doc_text;
+
mime_type = Some "text/plain"
+
};
+
annotations = None
+
}
+
};
+
Prompt.{
+
role = `Assistant;
+
content = make_text_content "I'll help summarize this document 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: 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)))
+
+
(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)))
+460 -1
lib/mcp.ml
···
open Jsonrpc
+
(* Standard error codes *)
+
module ErrorCode = struct
+
let parse_error = -32700
+
let invalid_request = -32600
+
let method_not_found = -32601
+
let invalid_params = -32602
+
let internal_error = -32603
+
let resource_not_found = -32002
+
let server_error_start = -32000
+
let server_error_end = -32099
+
end
+
(* Common types *)
module Role = struct
···
| j -> raise (Json.Of_json ("Expected object for ImageContent", j))
end
+
module AudioContent = struct
+
type t = {
+
data: string;
+
mime_type: string;
+
annotations: Annotated.annotation option;
+
}
+
+
let yojson_of_t { data; mime_type; annotations } =
+
let assoc = [
+
("data", `String data);
+
("mimeType", `String mime_type);
+
("type", `String "audio");
+
] in
+
let assoc = match annotations with
+
| Some annotations -> ("annotations", Annotated.yojson_of_annotation annotations) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let data = match List.assoc_opt "data" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'data' field", `Assoc fields))
+
in
+
let mime_type = match List.assoc_opt "mimeType" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'mimeType' field", `Assoc fields))
+
in
+
let _ = match List.assoc_opt "type" fields with
+
| Some (`String "audio") -> ()
+
| _ -> raise (Json.Of_json ("Missing or invalid 'type' field", `Assoc fields))
+
in
+
let annotations = List.assoc_opt "annotations" fields |> Option.map Annotated.annotation_of_yojson in
+
{ data; mime_type; annotations }
+
| j -> raise (Json.Of_json ("Expected object for AudioContent", j))
+
end
+
module ResourceContents = struct
type t = {
uri: string;
···
| j -> raise (Json.Of_json ("Expected object for EmbeddedResource", j))
end
+
(** Tool definition *)
+
module Tool = struct
+
type t = {
+
name: string;
+
description: string option;
+
input_schema: Json.t;
+
}
+
+
let yojson_of_t { name; description; input_schema } =
+
let assoc = [
+
("name", `String name);
+
("inputSchema", input_schema);
+
] in
+
let assoc = match description with
+
| Some desc -> ("description", `String desc) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'name' field", `Assoc fields))
+
in
+
let description = match List.assoc_opt "description" fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
in
+
let input_schema = match List.assoc_opt "inputSchema" fields with
+
| Some json -> json
+
| _ -> raise (Json.Of_json ("Missing 'inputSchema' field", `Assoc fields))
+
in
+
{ name; description; input_schema }
+
| j -> raise (Json.Of_json ("Expected object for Tool", j))
+
end
+
type content =
| Text of TextContent.t
| Image of ImageContent.t
+
| Audio of AudioContent.t
| Resource of EmbeddedResource.t
let yojson_of_content = function
| Text t -> TextContent.yojson_of_t t
| Image i -> ImageContent.yojson_of_t i
+
| Audio a -> AudioContent.yojson_of_t a
| Resource r -> EmbeddedResource.yojson_of_t r
let content_of_yojson = function
···
(match List.assoc_opt "type" fields with
| Some (`String "text") -> Text (TextContent.t_of_yojson (`Assoc fields))
| Some (`String "image") -> Image (ImageContent.t_of_yojson (`Assoc fields))
+
| Some (`String "audio") -> Audio (AudioContent.t_of_yojson (`Assoc fields))
| Some (`String "resource") -> Resource (EmbeddedResource.t_of_yojson (`Assoc fields))
| _ -> raise (Json.Of_json ("Invalid or missing content type", `Assoc fields)))
| j -> raise (Json.Of_json ("Expected object for content", j))
+
(** Tool result *)
+
module CallToolResult = struct
+
type t = {
+
content: content list;
+
is_error: bool;
+
meta: Json.t option;
+
}
+
+
let yojson_of_t { content; is_error; meta } =
+
let assoc = [
+
("content", `List (List.map yojson_of_content content));
+
("isError", `Bool is_error);
+
] in
+
let assoc = match meta with
+
| Some meta_json -> ("_meta", meta_json) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let content = match List.assoc_opt "content" fields with
+
| Some (`List items) -> List.map content_of_yojson items
+
| _ -> raise (Json.Of_json ("Missing or invalid 'content' field", `Assoc fields))
+
in
+
let is_error = match List.assoc_opt "isError" fields with
+
| Some (`Bool b) -> b
+
| None -> false (* Default to false if not specified *)
+
| _ -> raise (Json.Of_json ("Invalid 'isError' field", `Assoc fields))
+
in
+
let meta = List.assoc_opt "_meta" fields in
+
{ content; is_error; meta }
+
| j -> raise (Json.Of_json ("Expected object for CallToolResult", j))
+
end
+
+
(** Resource definition *)
+
module Resource = struct
+
type t = {
+
name: string;
+
uri: string;
+
description: string option;
+
mime_type: string option;
+
size: int option;
+
annotations: Annotated.annotation option;
+
}
+
+
let yojson_of_t { name; uri; description; mime_type; size; annotations } =
+
let assoc = [
+
("name", `String name);
+
("uri", `String uri);
+
] in
+
let assoc = match description with
+
| Some desc -> ("description", `String desc) :: assoc
+
| None -> assoc
+
in
+
let assoc = match mime_type with
+
| Some mime -> ("mimeType", `String mime) :: assoc
+
| None -> assoc
+
in
+
let assoc = match size with
+
| Some s -> ("size", `Int s) :: assoc
+
| None -> assoc
+
in
+
let assoc = match annotations with
+
| Some ann -> ("annotations", Annotated.yojson_of_annotation ann) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'name' field", `Assoc fields))
+
in
+
let uri = match List.assoc_opt "uri" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'uri' field", `Assoc fields))
+
in
+
let description = match List.assoc_opt "description" fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
in
+
let mime_type = match List.assoc_opt "mimeType" fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
in
+
let size = match List.assoc_opt "size" fields with
+
| Some (`Int s) -> Some s
+
| _ -> None
+
in
+
let annotations = match List.assoc_opt "annotations" fields with
+
| Some json -> Some (Annotated.annotation_of_yojson json)
+
| _ -> None
+
in
+
{ name; uri; description; mime_type; size; annotations }
+
| j -> raise (Json.Of_json ("Expected object for Resource", j))
+
end
+
+
(** Resource Template definition *)
+
module ResourceTemplate = struct
+
type t = {
+
name: string;
+
uri_template: string;
+
description: string option;
+
mime_type: string option;
+
annotations: Annotated.annotation option;
+
}
+
+
let yojson_of_t { name; uri_template; description; mime_type; annotations } =
+
let assoc = [
+
("name", `String name);
+
("uriTemplate", `String uri_template);
+
] in
+
let assoc = match description with
+
| Some desc -> ("description", `String desc) :: assoc
+
| None -> assoc
+
in
+
let assoc = match mime_type with
+
| Some mime -> ("mimeType", `String mime) :: assoc
+
| None -> assoc
+
in
+
let assoc = match annotations with
+
| Some ann -> ("annotations", Annotated.yojson_of_annotation ann) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'name' field", `Assoc fields))
+
in
+
let uri_template = match List.assoc_opt "uriTemplate" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'uriTemplate' field", `Assoc fields))
+
in
+
let description = match List.assoc_opt "description" fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
in
+
let mime_type = match List.assoc_opt "mimeType" fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
in
+
let annotations = match List.assoc_opt "annotations" fields with
+
| Some json -> Some (Annotated.annotation_of_yojson json)
+
| _ -> None
+
in
+
{ name; uri_template; description; mime_type; annotations }
+
| j -> raise (Json.Of_json ("Expected object for ResourceTemplate", j))
+
end
+
+
(** Resource Reference *)
+
module ResourceReference = struct
+
type t = {
+
uri: string;
+
}
+
+
let yojson_of_t { uri } =
+
`Assoc [
+
("type", `String "ref/resource");
+
("uri", `String uri);
+
]
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let _ = match List.assoc_opt "type" fields with
+
| Some (`String "ref/resource") -> ()
+
| _ -> raise (Json.Of_json ("Missing or invalid 'type' field", `Assoc fields))
+
in
+
let uri = match List.assoc_opt "uri" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'uri' field", `Assoc fields))
+
in
+
{ uri }
+
| j -> raise (Json.Of_json ("Expected object for ResourceReference", j))
+
end
+
+
(** Prompt Reference *)
+
module PromptReference = struct
+
type t = {
+
name: string;
+
}
+
+
let yojson_of_t { name } =
+
`Assoc [
+
("type", `String "ref/prompt");
+
("name", `String name);
+
]
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let _ = match List.assoc_opt "type" fields with
+
| Some (`String "ref/prompt") -> ()
+
| _ -> raise (Json.Of_json ("Missing or invalid 'type' field", `Assoc fields))
+
in
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'name' field", `Assoc fields))
+
in
+
{ name }
+
| j -> raise (Json.Of_json ("Expected object for PromptReference", j))
+
end
+
+
(** Completion support *)
+
module Completion = struct
+
+
module Argument = struct
+
type t = {
+
name: string;
+
value: string;
+
}
+
+
let yojson_of_t { name; value } =
+
`Assoc [
+
("name", `String name);
+
("value", `String value);
+
]
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'name' field", `Assoc fields))
+
in
+
let value = match List.assoc_opt "value" fields with
+
| Some (`String s) -> s
+
| _ -> raise (Json.Of_json ("Missing or invalid 'value' field", `Assoc fields))
+
in
+
{ name; value }
+
| j -> raise (Json.Of_json ("Expected object for Completion.Argument", j))
+
end
+
+
module Request = struct
+
type reference = [ `Prompt of PromptReference.t | `Resource of ResourceReference.t ]
+
+
type t = {
+
argument: Argument.t;
+
ref: reference;
+
}
+
+
let yojson_of_reference = function
+
| `Prompt p -> PromptReference.yojson_of_t p
+
| `Resource r -> ResourceReference.yojson_of_t r
+
+
let reference_of_yojson = function
+
| `Assoc fields ->
+
(match List.assoc_opt "type" fields with
+
| Some (`String "ref/prompt") -> `Prompt (PromptReference.t_of_yojson (`Assoc fields))
+
| Some (`String "ref/resource") -> `Resource (ResourceReference.t_of_yojson (`Assoc fields))
+
| _ -> raise (Json.Of_json ("Invalid or missing reference type", `Assoc fields)))
+
| j -> raise (Json.Of_json ("Expected object for reference", j))
+
+
let yojson_of_t { argument; ref } =
+
`Assoc [
+
("argument", Argument.yojson_of_t argument);
+
("ref", yojson_of_reference ref);
+
]
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let argument = match List.assoc_opt "argument" fields with
+
| Some json -> Argument.t_of_yojson json
+
| _ -> raise (Json.Of_json ("Missing argument field", `Assoc fields))
+
in
+
let ref = match List.assoc_opt "ref" fields with
+
| Some json -> reference_of_yojson json
+
| _ -> raise (Json.Of_json ("Missing ref field", `Assoc fields))
+
in
+
{ argument; ref }
+
| j -> raise (Json.Of_json ("Expected object for Completion.Request", j))
+
+
let create ~argument ~ref =
+
{ argument; ref }
+
+
let to_params t =
+
yojson_of_t t
+
end
+
+
module Result = struct
+
type completion = {
+
values: string list;
+
has_more: bool option;
+
total: int option;
+
}
+
+
type t = {
+
completion: completion;
+
meta: Json.t option;
+
}
+
+
let yojson_of_completion { values; has_more; total } =
+
let assoc = [
+
("values", `List (List.map (fun s -> `String s) values));
+
] in
+
let assoc = match has_more with
+
| Some b -> ("hasMore", `Bool b) :: assoc
+
| None -> assoc
+
in
+
let assoc = match total with
+
| Some n -> ("total", `Int n) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let completion_of_yojson = function
+
| `Assoc fields ->
+
let values = match List.assoc_opt "values" fields with
+
| Some (`List items) ->
+
List.map (function
+
| `String s -> s
+
| _ -> raise (Json.Of_json ("Expected string in values array", `List items))
+
) items
+
| _ -> raise (Json.Of_json ("Missing or invalid 'values' field", `Assoc fields))
+
in
+
let has_more = match List.assoc_opt "hasMore" fields with
+
| Some (`Bool b) -> Some b
+
| None -> None
+
| _ -> raise (Json.Of_json ("Invalid 'hasMore' field", `Assoc fields))
+
in
+
let total = match List.assoc_opt "total" fields with
+
| Some (`Int n) -> Some n
+
| None -> None
+
| _ -> raise (Json.Of_json ("Invalid 'total' field", `Assoc fields))
+
in
+
{ values; has_more; total }
+
| j -> raise (Json.Of_json ("Expected object for completion", j))
+
+
let yojson_of_t { completion; meta } =
+
let assoc = [
+
("completion", yojson_of_completion completion);
+
] in
+
let assoc = match meta with
+
| Some meta_json -> ("_meta", meta_json) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let completion = match List.assoc_opt "completion" fields with
+
| Some json -> completion_of_yojson json
+
| _ -> raise (Json.Of_json ("Missing completion field", `Assoc fields))
+
in
+
let meta = List.assoc_opt "_meta" fields in
+
{ completion; meta }
+
| j -> raise (Json.Of_json ("Expected object for Completion.Result", j))
+
+
let create ~completion ?meta () =
+
{ completion; meta }
+
+
let to_result t =
+
yojson_of_t t
+
end
+
end
+
(* Message types *)
module PromptMessage = struct
···
end
(* JSONRPC Message types *)
+
module JSONRPCMessage = struct
type notification = {
···
let create_notification = JSONRPCMessage.create_notification
let create_request = JSONRPCMessage.create_request
let create_response = JSONRPCMessage.create_response
-
let create_error = JSONRPCMessage.create_error
+
let create_error = JSONRPCMessage.create_error
+
+
(* Helper functions *)
+
let create_completion_request ~id ~argument ~ref =
+
let params = Completion.Request.to_params { argument; ref } in
+
create_request ~id ~method_:"completion/complete" ~params:(Some params) ()
+
+
let create_completion_response ~id ~values ?(has_more=None) ?(total=None) ?(meta=None) () =
+
let completion = { Completion.Result.values; has_more; total } in
+
let result = Completion.Result.to_result { completion; meta } in
+
create_response ~id ~result
+156 -1
lib/mcp.mli
···
open Jsonrpc
+
(** Standard error codes *)
+
module ErrorCode : sig
+
val parse_error : int
+
val invalid_request : int
+
val method_not_found : int
+
val invalid_params : int
+
val internal_error : int
+
val resource_not_found : int
+
val server_error_start : int
+
val server_error_end : int
+
end
+
(** Common types *)
(** Roles for conversation participants *)
···
val t_of_yojson : Json.t -> t
end
+
(** Audio content *)
+
module AudioContent : sig
+
type t = {
+
data: string;
+
mime_type: string;
+
annotations: Annotated.annotation option;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
(** Base resource contents *)
module ResourceContents : sig
type t = {
···
type content =
| Text of TextContent.t
| Image of ImageContent.t
+
| Audio of AudioContent.t
| Resource of EmbeddedResource.t
val yojson_of_content : content -> Json.t
···
end
end
+
(** Tool definition *)
+
module Tool : sig
+
type t = {
+
name: string;
+
description: string option;
+
input_schema: Json.t;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Tool result *)
+
module CallToolResult : sig
+
type t = {
+
content: content list;
+
is_error: bool;
+
meta: Json.t option;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Resource definition *)
+
module Resource : sig
+
type t = {
+
name: string;
+
uri: string;
+
description: string option;
+
mime_type: string option;
+
size: int option;
+
annotations: Annotated.annotation option;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Resource Template definition *)
+
module ResourceTemplate : sig
+
type t = {
+
name: string;
+
uri_template: string;
+
description: string option;
+
mime_type: string option;
+
annotations: Annotated.annotation option;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Resource Reference *)
+
module ResourceReference : sig
+
type t = {
+
uri: string;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Prompt Reference *)
+
module PromptReference : sig
+
type t = {
+
name: string;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
(** Completion support *)
+
module Completion : sig
+
module Argument : sig
+
type t = {
+
name: string;
+
value: string;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
end
+
+
module Request : sig
+
type reference = [ `Prompt of PromptReference.t | `Resource of ResourceReference.t ]
+
+
type t = {
+
argument: Argument.t;
+
ref: reference;
+
}
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
+
val yojson_of_reference : reference -> Json.t
+
val reference_of_yojson : Json.t -> reference
+
+
val create : argument:Argument.t -> ref:reference -> t
+
val to_params : t -> Json.t
+
end
+
+
module Result : sig
+
type completion = {
+
values: string list;
+
has_more: bool option;
+
total: int option;
+
}
+
+
type t = {
+
completion: completion;
+
meta: Json.t option;
+
}
+
+
val yojson_of_completion : completion -> Json.t
+
val completion_of_yojson : Json.t -> completion
+
+
val yojson_of_t : t -> Json.t
+
val t_of_yojson : Json.t -> t
+
+
val create : completion:completion -> ?meta:Json.t -> unit -> t
+
val to_result : t -> Json.t
+
end
+
end
+
(** Parse a JSON message into an MCP message *)
val parse_message : Json.t -> JSONRPCMessage.t
···
val create_notification : ?params:Json.t option -> method_:string -> unit -> JSONRPCMessage.t
val create_request : ?params:Json.t option -> ?progress_token:ProgressToken.t option -> id:RequestId.t -> method_:string -> unit -> JSONRPCMessage.t
val create_response : id:RequestId.t -> result:Json.t -> JSONRPCMessage.t
-
val create_error : id:RequestId.t -> code:int -> message:string -> ?data:Json.t option -> unit -> JSONRPCMessage.t
+
val create_error : id:RequestId.t -> code:int -> message:string -> ?data:Json.t option -> unit -> JSONRPCMessage.t
+
+
(** Helper functions for common requests/responses *)
+
val create_completion_request : id:RequestId.t -> argument:Completion.Argument.t -> ref:Completion.Request.reference -> JSONRPCMessage.t
+
val create_completion_response : id:RequestId.t -> values:string list -> ?has_more:bool option -> ?total:int option -> ?meta:Json.t option -> unit -> JSONRPCMessage.t
+248 -194
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
+
flush stderr
let debug = log Debug
let info = log Info
···
let report_progress ctx value total =
match ctx.progress_token, ctx.request_id with
-
| Some token, Some id ->
+
| Some token, Some _id ->
let params = `Assoc [
("progress", `Float value);
("total", `Float total);
···
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 [
···
("required", required_json)
]
-
(* Server implementation *)
-
module Server = struct
-
type startup_hook = unit -> unit
-
type shutdown_hook = unit -> unit
+
(* Main server type *)
+
type server = {
+
name: string;
+
version: string;
+
protocol_version: string;
+
mutable capabilities: Json.t;
+
mutable tools: Tool.t list;
+
mutable resources: Resource.t list;
+
mutable prompts: Prompt.t list;
+
mutable lifespan_context: (string * Json.t) list;
+
mutable startup_hook: (unit -> unit) option;
+
mutable shutdown_hook: (unit -> unit) option;
+
}
-
type t = {
-
name: string;
-
version: string;
-
protocol_version: string;
-
mutable capabilities: Json.t;
-
mutable tools: Tool.t list;
-
mutable resources: Resource.t list;
-
mutable prompts: Prompt.t list;
-
mutable lifespan_context: (string * Json.t) list;
-
startup_hook: startup_hook option;
-
shutdown_hook: shutdown_hook option;
+
(* Create a new server *)
+
let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () =
+
{
+
name;
+
version;
+
protocol_version;
+
capabilities = `Assoc [];
+
tools = [];
+
resources = [];
+
prompts = [];
+
lifespan_context = [];
+
startup_hook = None;
+
shutdown_hook = None;
}
-
let create ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") ?startup_hook ?shutdown_hook () =
-
{
-
name;
-
version;
-
protocol_version;
-
capabilities = `Assoc [];
-
tools = [];
-
resources = [];
-
prompts = [];
-
lifespan_context = [];
-
startup_hook;
-
shutdown_hook;
-
}
+
(* Default capabilities for the server *)
+
let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
+
let caps = [] in
+
let caps =
+
if with_tools then
+
("tools", `Assoc [
+
("listChanged", `Bool true)
+
]) :: caps
+
else
+
caps
+
in
+
let caps =
+
if with_resources then
+
("resources", `Assoc [
+
("listChanged", `Bool true);
+
("subscribe", `Bool false)
+
]) :: caps
+
else if not with_resources then
+
("resources", `Assoc [
+
("listChanged", `Bool false);
+
("subscribe", `Bool false)
+
]) :: caps
+
else
+
caps
+
in
+
let caps =
+
if with_prompts then
+
("prompts", `Assoc [
+
("listChanged", `Bool true)
+
]) :: caps
+
else if not with_prompts then
+
("prompts", `Assoc [
+
("listChanged", `Bool false)
+
]) :: caps
+
else
+
caps
+
in
+
`Assoc caps
-
(* Register a tool *)
-
let register_tool server tool =
-
server.tools <- tool :: server.tools;
-
()
+
(* Register a tool *)
+
let register_tool server tool =
+
server.tools <- tool :: server.tools;
+
tool
-
(* Register a resource *)
-
let register_resource server resource =
-
server.resources <- resource :: server.resources;
+
(* Create and register a tool in one step *)
+
let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler =
+
let input_schema = make_tool_schema schema_properties schema_required in
+
let handler' ctx args =
+
try
+
Ok (handler args)
+
with exn ->
+
Error (Printexc.to_string exn)
+
in
+
let tool = Tool.create
+
~name
+
?description
+
~input_schema
+
~handler:handler'
()
+
in
+
register_tool server tool
-
(* Register a prompt *)
-
let register_prompt server prompt =
-
server.prompts <- prompt :: server.prompts;
+
(* Register a resource *)
+
let register_resource server resource =
+
server.resources <- resource :: server.resources;
+
resource
+
+
(* Create and register a resource in one step *)
+
let add_resource server ~uri_template ?description ?mime_type handler =
+
let handler' _ctx params =
+
try
+
Ok (handler params)
+
with exn ->
+
Error (Printexc.to_string exn)
+
in
+
let resource = Resource.create
+
~uri_template
+
?description
+
?mime_type
+
~handler:handler'
()
-
-
(* Default server capabilities *)
-
let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
-
let caps = [] in
-
let caps =
-
if with_tools then
-
("tools", `Assoc [
-
("listChanged", `Bool true)
-
]) :: caps
-
else
-
caps
-
in
-
let caps =
-
if with_resources then
-
("resources", `Assoc [
-
("listChanged", `Bool true);
-
("subscribe", `Bool false)
-
]) :: caps
-
else if not with_resources then
-
("resources", `Assoc [
-
("listChanged", `Bool false);
-
("subscribe", `Bool false)
-
]) :: caps
-
else
-
caps
-
in
-
let caps =
-
if with_prompts then
-
("prompts", `Assoc [
-
("listChanged", `Bool true)
-
]) :: caps
-
else if not with_prompts then
-
("prompts", `Assoc [
-
("listChanged", `Bool false)
-
]) :: caps
-
else
-
caps
-
in
-
`Assoc caps
+
in
+
register_resource server resource
-
(* Update server capabilities *)
-
let update_capabilities server capabilities =
-
server.capabilities <- capabilities
+
(* Register a prompt *)
+
let register_prompt server prompt =
+
server.prompts <- prompt :: server.prompts;
+
prompt
-
(* Process a message *)
-
let process_message _server _json =
-
None
-
-
(* Main server loop *)
-
let run _server =
-
(* Placeholder implementation *)
+
(* Create and register a prompt in one step *)
+
let add_prompt server ~name ?description ?(arguments=[]) handler =
+
let prompt_args = List.map (fun (name, desc, required) ->
+
Prompt.create_argument ~name ?description:desc ~required ()
+
) arguments in
+
let handler' _ctx args =
+
try
+
Ok (handler args)
+
with exn ->
+
Error (Printexc.to_string exn)
+
in
+
let prompt = Prompt.create
+
~name
+
?description
+
~arguments:prompt_args
+
~handler:handler'
()
-
end
+
in
+
register_prompt server prompt
+
+
(* Set server capabilities *)
+
let set_capabilities server capabilities =
+
server.capabilities <- capabilities
+
+
(* Configure server with default capabilities based on registered components *)
+
let configure_server server ?with_tools ?with_resources ?with_prompts () =
+
let with_tools = match with_tools with
+
| Some b -> b
+
| None -> server.tools <> []
+
in
+
let with_resources = match with_resources with
+
| Some b -> b
+
| None -> server.resources <> []
+
in
+
let with_prompts = match with_prompts with
+
| Some b -> b
+
| None -> server.prompts <> []
+
in
+
let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in
+
set_capabilities server capabilities;
+
server
+
+
(* Set startup and shutdown hooks *)
+
let set_startup_hook server hook =
+
server.startup_hook <- Some hook
+
+
let set_shutdown_hook server hook =
+
server.shutdown_hook <- Some hook
-
(* Helper function for default capabilities *)
-
let default_capabilities = Server.default_capabilities
+
(* Transport type for server *)
+
type transport_type =
+
| Stdio (* Read/write to stdin/stdout *)
+
| Http (* HTTP server - to be implemented *)
-
(* Add syntactic sugar for creating a server *)
-
module MakeServer(S: sig val name: string val version: string option end) = struct
-
let _config = (S.name, S.version) (* Used to prevent unused parameter warning *)
+
(* 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);
+
+
(* Initialize capabilities if not already set *)
+
if server.capabilities = `Assoc [] then
+
ignore (configure_server server ());
+
+
(* Run startup hook if provided *)
+
(match server.startup_hook with
+
| Some hook -> hook ()
+
| None -> ());
-
(* Create server *)
-
let server = Server.create
-
~name:S.name
-
?version:S.version
-
~protocol_version:"2024-11-05"
-
()
-
-
(* Create a tool *)
-
let tool ?name ?description ?(schema_properties=[]) ?(schema_required=[]) handler =
-
let name = match name with
-
| Some (Some n) -> n
-
| Some None | None -> "tool" in
-
let input_schema = make_tool_schema schema_properties schema_required in
-
let handler' ctx args =
-
try
-
Ok (handler args)
-
with exn ->
-
Error (Printexc.to_string exn)
-
in
-
let tool = Tool.create
-
~name
-
?description
-
~input_schema
-
~handler:handler'
-
()
-
in
-
server.tools <- tool :: server.tools;
-
tool
-
-
(* Create a resource *)
-
let resource ?uri_template ?description ?mime_type handler =
-
let uri_template = match uri_template with
-
| Some (Some uri) -> uri
-
| Some None | None -> "resource://" in
-
let handler' ctx params =
-
try
-
Ok (handler params)
-
with exn ->
-
Error (Printexc.to_string exn)
-
in
-
let resource = Resource.create
-
~uri_template
-
?description
-
?mime_type
-
~handler:handler'
-
()
-
in
-
server.resources <- resource :: server.resources;
-
resource
-
-
(* Create a prompt *)
-
let prompt ?name ?description ?(arguments=[]) handler =
-
let name = match name with
-
| Some (Some n) -> n
-
| Some None | None -> "prompt" in
-
let prompt_args = List.map (fun (name, desc, required) ->
-
Prompt.create_argument ~name ?description:desc ~required ()
-
) arguments in
-
let handler' ctx args =
-
try
-
Ok (handler args)
-
with exn ->
-
Error (Printexc.to_string exn)
-
in
-
let prompt = Prompt.create
-
~name
-
?description
-
~arguments:prompt_args
-
~handler:handler'
-
()
-
in
-
server.prompts <- prompt :: server.prompts;
-
prompt
-
-
(* Run the server *)
-
let run ?with_tools ?with_resources ?with_prompts () =
-
let with_tools = match with_tools with
-
| Some b -> b
-
| None -> server.tools <> []
-
in
-
let with_resources = match with_resources with
-
| Some b -> b
-
| None -> server.resources <> []
-
in
-
let with_prompts = match with_prompts with
-
| Some b -> b
-
| None -> server.prompts <> []
-
in
-
let capabilities = Server.default_capabilities ~with_tools ~with_resources ~with_prompts () in
-
server.capabilities <- capabilities;
-
Log.info "Starting server...";
-
Log.info (Printf.sprintf "Server info: %s v%s" server.name
-
(match S.version with Some v -> v | None -> "unknown"));
-
Printexc.record_backtrace true;
-
set_binary_mode_out stdout false;
-
Log.info "This is just a placeholder server implementation."
-
end
+
(* 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
+67 -40
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 yojson_of_message : message -> Json.t
+
val message_of_yojson : Json.t -> message
val to_json : t -> Json.t
end
-
(** Server implementation *)
-
module Server : sig
-
type startup_hook = unit -> unit
-
type shutdown_hook = unit -> unit
+
(** Main server type *)
+
type server = {
+
name: string;
+
version: string;
+
protocol_version: string;
+
mutable capabilities: Json.t;
+
mutable tools: Tool.t list;
+
mutable resources: Resource.t list;
+
mutable prompts: Prompt.t list;
+
mutable lifespan_context: (string * Json.t) list;
+
mutable startup_hook: (unit -> unit) option;
+
mutable shutdown_hook: (unit -> unit) option;
+
}
+
+
(** Create a new server *)
+
val create_server : name:string -> ?version:string -> ?protocol_version:string -> unit -> server
+
+
(** Default capabilities for the server *)
+
val default_capabilities : ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> Json.t
+
+
(** Register a tool with the server *)
+
val register_tool : server -> Tool.t -> Tool.t
+
+
(** Create and register a tool in one step *)
+
val add_tool : server -> name:string -> ?description:string -> ?schema_properties:(string * string * string) list -> ?schema_required:string list -> (Json.t -> Json.t) -> Tool.t
+
+
(** Register a resource with the server *)
+
val register_resource : server -> Resource.t -> Resource.t
+
+
(** Create and register a resource in one step *)
+
val add_resource : server -> uri_template:string -> ?description:string -> ?mime_type:string -> (string list -> string) -> Resource.t
+
+
(** Register a prompt with the server *)
+
val register_prompt : server -> Prompt.t -> Prompt.t
+
+
(** Create and register a prompt in one step *)
+
val add_prompt : server -> name:string -> ?description:string -> ?arguments:(string * string option * bool) list -> ((string * string) list -> Prompt.message list) -> Prompt.t
+
+
(** Set server capabilities *)
+
val set_capabilities : server -> Json.t -> unit
+
+
(** Configure server with default capabilities based on registered components *)
+
val configure_server : server -> ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> server
+
+
(** Set startup hook *)
+
val set_startup_hook : server -> (unit -> unit) -> unit
+
+
(** Set shutdown hook *)
+
val set_shutdown_hook : server -> (unit -> unit) -> unit
-
type t = {
-
name: string;
-
version: string;
-
protocol_version: string;
-
mutable capabilities: Json.t;
-
mutable tools: Tool.t list;
-
mutable resources: Resource.t list;
-
mutable prompts: Prompt.t list;
-
mutable lifespan_context: (string * Json.t) list;
-
startup_hook: startup_hook option;
-
shutdown_hook: shutdown_hook option;
-
}
+
(** Run the server using stdio transport (legacy method) *)
+
val run_server : server -> unit
-
val create : name:string -> ?version:string -> ?protocol_version:string -> ?startup_hook:startup_hook -> ?shutdown_hook:shutdown_hook -> unit -> t
-
val register_tool : t -> Tool.t -> unit
-
val register_resource : t -> Resource.t -> unit
-
val register_prompt : t -> Prompt.t -> unit
-
val default_capabilities : ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> Json.t
-
val update_capabilities : t -> Json.t -> unit
+
(** Transport type for the server *)
+
type transport_type =
+
| Stdio (** Read/write to stdin/stdout *)
+
| Http (** HTTP server - to be implemented *)
-
val process_message : t -> Json.t -> JSONRPCMessage.t option
-
val run : t -> unit
-
end
+
(** 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_tool_schema : (string * string * string) list -> string list -> Json.t
-
-
(** Syntax sugar for creating an MCP server *)
-
module MakeServer : functor (S : sig
-
val name : string
-
val version : string option
-
end) -> sig
-
val _config : string * string option (* Used to prevent unused parameter warning *)
-
val server : Server.t
-
-
val tool : ?name:string option -> ?description:string -> ?schema_properties:(string * string * string) list -> ?schema_required:string list -> (Json.t -> Json.t) -> Tool.t
-
val resource : ?uri_template:string option -> ?description:string -> ?mime_type:string -> (string list -> string) -> Resource.t
-
val prompt : ?name:string option -> ?description:string -> ?arguments:(string * string option * bool) list -> ((string * string) list -> Prompt.message list) -> Prompt.t
-
val run : ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> unit
-
end
+
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
+178
spec/00-arch.md
···
+
---
+
title: Architecture
+
cascade:
+
type: docs
+
weight: 10
+
---
+
+
The Model Context Protocol (MCP) follows a client-host-server architecture where each
+
host can run multiple client instances. This architecture enables users to integrate AI
+
capabilities across applications while maintaining clear security boundaries and
+
isolating concerns. Built on JSON-RPC, MCP provides a stateful session protocol focused
+
on context exchange and sampling coordination between clients and servers.
+
+
## Core Components
+
+
```mermaid
+
graph LR
+
subgraph "Application Host Process"
+
H[Host]
+
C1[Client 1]
+
C2[Client 2]
+
C3[Client 3]
+
H --> C1
+
H --> C2
+
H --> C3
+
end
+
+
subgraph "Local machine"
+
S1[Server 1<br>Files & Git]
+
S2[Server 2<br>Database]
+
R1[("Local<br>Resource A")]
+
R2[("Local<br>Resource B")]
+
+
C1 --> S1
+
C2 --> S2
+
S1 <--> R1
+
S2 <--> R2
+
end
+
+
subgraph "Internet"
+
S3[Server 3<br>External APIs]
+
R3[("Remote<br>Resource C")]
+
+
C3 --> S3
+
S3 <--> R3
+
end
+
```
+
+
### Host
+
+
The host process acts as the container and coordinator:
+
+
- Creates and manages multiple client instances
+
- Controls client connection permissions and lifecycle
+
- Enforces security policies and consent requirements
+
- Handles user authorization decisions
+
- Coordinates AI/LLM integration and sampling
+
- Manages context aggregation across clients
+
+
### Clients
+
+
Each client is created by the host and maintains an isolated server connection:
+
+
- Establishes one stateful session per server
+
- Handles protocol negotiation and capability exchange
+
- Routes protocol messages bidirectionally
+
- Manages subscriptions and notifications
+
- Maintains security boundaries between servers
+
+
A host application creates and manages multiple clients, with each client having a 1:1
+
relationship with a particular server.
+
+
### Servers
+
+
Servers provide specialized context and capabilities:
+
+
- Expose resources, tools and prompts via MCP primitives
+
- Operate independently with focused responsibilities
+
- Request sampling through client interfaces
+
- Must respect security constraints
+
- Can be local processes or remote services
+
+
## Design Principles
+
+
MCP is built on several key design principles that inform its architecture and
+
implementation:
+
+
1. **Servers should be extremely easy to build**
+
+
- Host applications handle complex orchestration responsibilities
+
- Servers focus on specific, well-defined capabilities
+
- Simple interfaces minimize implementation overhead
+
- Clear separation enables maintainable code
+
+
2. **Servers should be highly composable**
+
+
- Each server provides focused functionality in isolation
+
- Multiple servers can be combined seamlessly
+
- Shared protocol enables interoperability
+
- Modular design supports extensibility
+
+
3. **Servers should not be able to read the whole conversation, nor "see into" other
+
servers**
+
+
- Servers receive only necessary contextual information
+
- Full conversation history stays with the host
+
- Each server connection maintains isolation
+
- Cross-server interactions are controlled by the host
+
- Host process enforces security boundaries
+
+
4. **Features can be added to servers and clients progressively**
+
- Core protocol provides minimal required functionality
+
- Additional capabilities can be negotiated as needed
+
- Servers and clients evolve independently
+
- Protocol designed for future extensibility
+
- Backwards compatibility is maintained
+
+
## Capability Negotiation
+
+
The Model Context Protocol uses a capability-based negotiation system where clients and
+
servers explicitly declare their supported features during initialization. Capabilities
+
determine which protocol features and primitives are available during a session.
+
+
- Servers declare capabilities like resource subscriptions, tool support, and prompt
+
templates
+
- Clients declare capabilities like sampling support and notification handling
+
- Both parties must respect declared capabilities throughout the session
+
- Additional capabilities can be negotiated through extensions to the protocol
+
+
```mermaid
+
sequenceDiagram
+
participant Host
+
participant Client
+
participant Server
+
+
Host->>+Client: Initialize client
+
Client->>+Server: Initialize session with capabilities
+
Server-->>Client: Respond with supported capabilities
+
+
Note over Host,Server: Active Session with Negotiated Features
+
+
loop Client Requests
+
Host->>Client: User- or model-initiated action
+
Client->>Server: Request (tools/resources)
+
Server-->>Client: Response
+
Client-->>Host: Update UI or respond to model
+
end
+
+
loop Server Requests
+
Server->>Client: Request (sampling)
+
Client->>Host: Forward to AI
+
Host-->>Client: AI response
+
Client-->>Server: Response
+
end
+
+
loop Notifications
+
Server--)Client: Resource updates
+
Client--)Server: Status changes
+
end
+
+
Host->>Client: Terminate
+
Client->>-Server: End session
+
deactivate Server
+
```
+
+
Each capability unlocks specific protocol features for use during the session. For
+
example:
+
+
- Implemented [server features]({{< ref "../server" >}}) must be advertised in the
+
server's capabilities
+
- Emitting resource subscription notifications requires the server to declare
+
subscription support
+
- Tool invocation requires the server to declare tool capabilities
+
- [Sampling]({{< ref "../client" >}}) requires the client to declare support in its
+
capabilities
+
+
This capability negotiation ensures clients and servers have a clear understanding of
+
supported functionality while maintaining protocol extensibility.
+4
spec/README.md
···
+
These specifications are snapshots from
+
https://github.com/modelcontextprotocol/specification/tree/main/docs/specification/2025-03-26
+
+
to help with the agentic prompting.
+386
spec/authorization.md
···
+
---
+
title: Authorization
+
type: docs
+
weight: 15
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
## 1. Introduction
+
+
### 1.1 Purpose and Scope
+
+
The Model Context Protocol provides authorization capabilities at the transport level,
+
enabling MCP clients to make requests to restricted MCP servers on behalf of resource
+
owners. This specification defines the authorization flow for HTTP-based transports.
+
+
### 1.2 Protocol Requirements
+
+
Authorization is **OPTIONAL** for MCP implementations. When supported:
+
+
- Implementations using an HTTP-based transport **SHOULD** conform to this specification.
+
- Implementations using an STDIO transport **SHOULD NOT** follow this specification, and
+
instead retrieve credentials from the environment.
+
- Implementations using alternative transports **MUST** follow established security best
+
practices for their protocol.
+
+
### 1.3 Standards Compliance
+
+
This authorization mechanism is based on established specifications listed below, but
+
implements a selected subset of their features to ensure security and interoperability
+
while maintaining simplicity:
+
+
- [OAuth 2.1 IETF DRAFT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12)
+
- OAuth 2.0 Authorization Server Metadata
+
([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414))
+
- OAuth 2.0 Dynamic Client Registration Protocol
+
([RFC7591](https://datatracker.ietf.org/doc/html/rfc7591))
+
+
## 2. Authorization Flow
+
+
### 2.1 Overview
+
+
1. MCP auth implementations **MUST** implement OAuth 2.1 with appropriate security
+
measures for both confidential and public clients.
+
+
2. MCP auth implementations **SHOULD** support the OAuth 2.0 Dynamic Client Registration
+
Protocol ([RFC7591](https://datatracker.ietf.org/doc/html/rfc7591)).
+
+
3. MCP servers **SHOULD** and MCP clients **MUST** implement OAuth 2.0 Authorization
+
Server Metadata ([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414)). Servers
+
that do not support Authorization Server Metadata **MUST** follow the default URI
+
schema.
+
+
### 2.2 Basic OAuth 2.1 Authorization
+
+
When authorization is required and not yet proven by the client, servers **MUST** respond
+
with _HTTP 401 Unauthorized_.
+
+
Clients initiate the
+
[OAuth 2.1 IETF DRAFT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12)
+
authorization flow after receiving the _HTTP 401 Unauthorized_.
+
+
The following demonstrates the basic OAuth 2.1 for public clients using PKCE.
+
+
```mermaid
+
sequenceDiagram
+
participant B as User-Agent (Browser)
+
participant C as Client
+
participant M as MCP Server
+
+
C->>M: MCP Request
+
M->>C: HTTP 401 Unauthorized
+
Note over C: Generate code_verifier and code_challenge
+
C->>B: Open browser with authorization URL + code_challenge
+
B->>M: GET /authorize
+
Note over M: User logs in and authorizes
+
M->>B: Redirect to callback URL with auth code
+
B->>C: Callback with authorization code
+
C->>M: Token Request with code + code_verifier
+
M->>C: Access Token (+ Refresh Token)
+
C->>M: MCP Request with Access Token
+
Note over C,M: Begin standard MCP message exchange
+
```
+
+
### 2.3 Server Metadata Discovery
+
+
For server capability discovery:
+
+
- MCP clients _MUST_ follow the OAuth 2.0 Authorization Server Metadata protocol defined
+
in [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414).
+
- MCP server _SHOULD_ follow the OAuth 2.0 Authorization Server Metadata protocol.
+
- MCP servers that do not support the OAuth 2.0 Authorization Server Metadata protocol,
+
_MUST_ support fallback URLs.
+
+
The discovery flow is illustrated below:
+
+
```mermaid
+
sequenceDiagram
+
participant C as Client
+
participant S as Server
+
+
C->>S: GET /.well-known/oauth-authorization-server
+
alt Discovery Success
+
S->>C: 200 OK + Metadata Document
+
Note over C: Use endpoints from metadata
+
else Discovery Failed
+
S->>C: 404 Not Found
+
Note over C: Fall back to default endpoints
+
end
+
Note over C: Continue with authorization flow
+
```
+
+
#### 2.3.1 Server Metadata Discovery Headers
+
+
MCP clients _SHOULD_ include the header `MCP-Protocol-Version: <protocol-version>` during
+
Server Metadata Discovery to allow the MCP server to respond based on the MCP protocol
+
version.
+
+
For example: `MCP-Protocol-Version: 2024-11-05`
+
+
#### 2.3.2 Authorization Base URL
+
+
The authorization base URL **MUST** be determined from the MCP server URL by discarding
+
any existing `path` component. For example:
+
+
If the MCP server URL is `https://api.example.com/v1/mcp`, then:
+
+
- The authorization base URL is `https://api.example.com`
+
- The metadata endpoint **MUST** be at
+
`https://api.example.com/.well-known/oauth-authorization-server`
+
+
This ensures authorization endpoints are consistently located at the root level of the
+
domain hosting the MCP server, regardless of any path components in the MCP server URL.
+
+
#### 2.3.3 Fallbacks for Servers without Metadata Discovery
+
+
For servers that do not implement OAuth 2.0 Authorization Server Metadata, clients
+
**MUST** use the following default endpoint paths relative to the authorization base URL
+
(as defined in [Section 2.3.2](#232-authorization-base-url)):
+
+
| Endpoint | Default Path | Description |
+
| ---------------------- | ------------ | ------------------------------------ |
+
| Authorization Endpoint | /authorize | Used for authorization requests |
+
| Token Endpoint | /token | Used for token exchange & refresh |
+
| Registration Endpoint | /register | Used for dynamic client registration |
+
+
For example, with an MCP server hosted at `https://api.example.com/v1/mcp`, the default
+
endpoints would be:
+
+
- `https://api.example.com/authorize`
+
- `https://api.example.com/token`
+
- `https://api.example.com/register`
+
+
Clients **MUST** first attempt to discover endpoints via the metadata document before
+
falling back to default paths. When using default paths, all other protocol requirements
+
remain unchanged.
+
+
### 2.3 Dynamic Client Registration
+
+
MCP clients and servers **SHOULD** support the
+
[OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
+
to allow MCP clients to obtain OAuth client IDs without user interaction. This provides a
+
standardized way for clients to automatically register with new servers, which is crucial
+
for MCP because:
+
+
- Clients cannot know all possible servers in advance
+
- Manual registration would create friction for users
+
- It enables seamless connection to new servers
+
- Servers can implement their own registration policies
+
+
Any MCP servers that _do not_ support Dynamic Client Registration need to provide
+
alternative ways to obtain a client ID (and, if applicable, client secret). For one of
+
these servers, MCP clients will have to either:
+
+
1. Hardcode a client ID (and, if applicable, client secret) specifically for that MCP
+
server, or
+
2. Present a UI to users that allows them to enter these details, after registering an
+
OAuth client themselves (e.g., through a configuration interface hosted by the
+
server).
+
+
### 2.4 Authorization Flow Steps
+
+
The complete Authorization flow proceeds as follows:
+
+
```mermaid
+
sequenceDiagram
+
participant B as User-Agent (Browser)
+
participant C as Client
+
participant M as MCP Server
+
+
C->>M: GET /.well-known/oauth-authorization-server
+
alt Server Supports Discovery
+
M->>C: Authorization Server Metadata
+
else No Discovery
+
M->>C: 404 (Use default endpoints)
+
end
+
+
alt Dynamic Client Registration
+
C->>M: POST /register
+
M->>C: Client Credentials
+
end
+
+
Note over C: Generate PKCE Parameters
+
C->>B: Open browser with authorization URL + code_challenge
+
B->>M: Authorization Request
+
Note over M: User /authorizes
+
M->>B: Redirect to callback with authorization code
+
B->>C: Authorization code callback
+
C->>M: Token Request + code_verifier
+
M->>C: Access Token (+ Refresh Token)
+
C->>M: API Requests with Access Token
+
```
+
+
#### 2.4.1 Decision Flow Overview
+
+
```mermaid
+
flowchart TD
+
A[Start Auth Flow] --> B{Check Metadata Discovery}
+
B -->|Available| C[Use Metadata Endpoints]
+
B -->|Not Available| D[Use Default Endpoints]
+
+
C --> G{Check Registration Endpoint}
+
D --> G
+
+
G -->|Available| H[Perform Dynamic Registration]
+
G -->|Not Available| I[Alternative Registration Required]
+
+
H --> J[Start OAuth Flow]
+
I --> J
+
+
J --> K[Generate PKCE Parameters]
+
K --> L[Request Authorization]
+
L --> M[User Authorization]
+
M --> N[Exchange Code for Tokens]
+
N --> O[Use Access Token]
+
```
+
+
### 2.5 Access Token Usage
+
+
#### 2.5.1 Token Requirements
+
+
Access token handling **MUST** conform to
+
[OAuth 2.1 Section 5](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-5)
+
requirements for resource requests. Specifically:
+
+
1. MCP client **MUST** use the Authorization request header field
+
[Section 5.1.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-5.1.1):
+
+
```
+
Authorization: Bearer <access-token>
+
```
+
+
Note that authorization **MUST** be included in every HTTP request from client to server,
+
even if they are part of the same logical session.
+
+
2. Access tokens **MUST NOT** be included in the URI query string
+
+
Example request:
+
+
```http
+
GET /v1/contexts HTTP/1.1
+
Host: mcp.example.com
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
+
```
+
+
#### 2.5.2 Token Handling
+
+
Resource servers **MUST** validate access tokens as described in
+
[Section 5.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-5.2).
+
If validation fails, servers **MUST** respond according to
+
[Section 5.3](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-5.3)
+
error handling requirements. Invalid or expired tokens **MUST** receive a HTTP 401
+
response.
+
+
### 2.6 Security Considerations
+
+
The following security requirements **MUST** be implemented:
+
+
1. Clients **MUST** securely store tokens following OAuth 2.0 best practices
+
2. Servers **SHOULD** enforce token expiration and rotation
+
3. All authorization endpoints **MUST** be served over HTTPS
+
4. Servers **MUST** validate redirect URIs to prevent open redirect vulnerabilities
+
5. Redirect URIs **MUST** be either localhost URLs or HTTPS URLs
+
+
### 2.7 Error Handling
+
+
Servers **MUST** return appropriate HTTP status codes for authorization errors:
+
+
| Status Code | Description | Usage |
+
| ----------- | ------------ | ------------------------------------------ |
+
| 401 | Unauthorized | Authorization required or token invalid |
+
| 403 | Forbidden | Invalid scopes or insufficient permissions |
+
| 400 | Bad Request | Malformed authorization request |
+
+
### 2.8 Implementation Requirements
+
+
1. Implementations **MUST** follow OAuth 2.1 security best practices
+
2. PKCE is **REQUIRED** for all clients
+
3. Token rotation **SHOULD** be implemented for enhanced security
+
4. Token lifetimes **SHOULD** be limited based on security requirements
+
+
### 2.9 Third-Party Authorization Flow
+
+
#### 2.9.1 Overview
+
+
MCP servers **MAY** support delegated authorization through third-party authorization
+
servers. In this flow, the MCP server acts as both an OAuth client (to the third-party
+
auth server) and an OAuth authorization server (to the MCP client).
+
+
#### 2.9.2 Flow Description
+
+
The third-party authorization flow comprises these steps:
+
+
1. MCP client initiates standard OAuth flow with MCP server
+
2. MCP server redirects user to third-party authorization server
+
3. User authorizes with third-party server
+
4. Third-party server redirects back to MCP server with authorization code
+
5. MCP server exchanges code for third-party access token
+
6. MCP server generates its own access token bound to the third-party session
+
7. MCP server completes original OAuth flow with MCP client
+
+
```mermaid
+
sequenceDiagram
+
participant B as User-Agent (Browser)
+
participant C as MCP Client
+
participant M as MCP Server
+
participant T as Third-Party Auth Server
+
+
C->>M: Initial OAuth Request
+
M->>B: Redirect to Third-Party /authorize
+
B->>T: Authorization Request
+
Note over T: User authorizes
+
T->>B: Redirect to MCP Server callback
+
B->>M: Authorization code
+
M->>T: Exchange code for token
+
T->>M: Third-party access token
+
Note over M: Generate bound MCP token
+
M->>B: Redirect to MCP Client callback
+
B->>C: MCP authorization code
+
C->>M: Exchange code for token
+
M->>C: MCP access token
+
```
+
+
#### 2.9.3 Session Binding Requirements
+
+
MCP servers implementing third-party authorization **MUST**:
+
+
1. Maintain secure mapping between third-party tokens and issued MCP tokens
+
2. Validate third-party token status before honoring MCP tokens
+
3. Implement appropriate token lifecycle management
+
4. Handle third-party token expiration and renewal
+
+
#### 2.9.4 Security Considerations
+
+
When implementing third-party authorization, servers **MUST**:
+
+
1. Validate all redirect URIs
+
2. Securely store third-party credentials
+
3. Implement appropriate session timeout handling
+
4. Consider security implications of token chaining
+
5. Implement proper error handling for third-party auth failures
+
+
## 3. Best Practices
+
+
#### 3.1 Local clients as Public OAuth 2.1 Clients
+
+
We strongly recommend that local clients implement OAuth 2.1 as a public client:
+
+
1. Utilizing code challenges (PKCE) for authorization requests to prevent interception
+
attacks
+
2. Implementing secure token storage appropriate for the local system
+
3. Following token refresh best practices to maintain sessions
+
4. Properly handling token expiration and renewal
+
+
#### 3.2 Authorization Metadata Discovery
+
+
We strongly recommend that all clients implement metadata discovery. This reduces the
+
need for users to provide endpoints manually or clients to fallback to the defined
+
defaults.
+
+
#### 3.3 Dynamic Client Registration
+
+
Since clients do not know the set of MCP servers in advance, we strongly recommend the
+
implementation of dynamic client registration. This allows applications to automatically
+
register with the MCP server, and removes the need for users to obtain client ids
+
manually.
+239
spec/lifecycle.md
···
+
---
+
title: Lifecycle
+
type: docs
+
weight: 30
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
The Model Context Protocol (MCP) defines a rigorous lifecycle for client-server
+
connections that ensures proper capability negotiation and state management.
+
+
1. **Initialization**: Capability negotiation and protocol version agreement
+
2. **Operation**: Normal protocol communication
+
3. **Shutdown**: Graceful termination of the connection
+
+
```mermaid
+
sequenceDiagram
+
participant Client
+
participant Server
+
+
Note over Client,Server: Initialization Phase
+
activate Client
+
Client->>+Server: initialize request
+
Server-->>Client: initialize response
+
Client--)Server: initialized notification
+
+
Note over Client,Server: Operation Phase
+
rect rgb(200, 220, 250)
+
note over Client,Server: Normal protocol operations
+
end
+
+
Note over Client,Server: Shutdown
+
Client--)-Server: Disconnect
+
deactivate Server
+
Note over Client,Server: Connection closed
+
```
+
+
## Lifecycle Phases
+
+
### Initialization
+
+
The initialization phase **MUST** be the first interaction between client and server.
+
During this phase, the client and server:
+
+
- Establish protocol version compatibility
+
- Exchange and negotiate capabilities
+
- Share implementation details
+
+
The client **MUST** initiate this phase by sending an `initialize` request containing:
+
+
- Protocol version supported
+
- Client capabilities
+
- Client implementation information
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"method": "initialize",
+
"params": {
+
"protocolVersion": "2024-11-05",
+
"capabilities": {
+
"roots": {
+
"listChanged": true
+
},
+
"sampling": {}
+
},
+
"clientInfo": {
+
"name": "ExampleClient",
+
"version": "1.0.0"
+
}
+
}
+
}
+
```
+
+
The initialize request **MUST NOT** be part of a JSON-RPC
+
[batch](https://www.jsonrpc.org/specification#batch), as other requests and notifications
+
are not possible until initialization has completed. This also permits backwards
+
compatibility with prior protocol versions that do not explicitly support JSON-RPC
+
batches.
+
+
The server **MUST** respond with its own capabilities and information:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"result": {
+
"protocolVersion": "2024-11-05",
+
"capabilities": {
+
"logging": {},
+
"prompts": {
+
"listChanged": true
+
},
+
"resources": {
+
"subscribe": true,
+
"listChanged": true
+
},
+
"tools": {
+
"listChanged": true
+
}
+
},
+
"serverInfo": {
+
"name": "ExampleServer",
+
"version": "1.0.0"
+
}
+
}
+
}
+
```
+
+
After successful initialization, the client **MUST** send an `initialized` notification
+
to indicate it is ready to begin normal operations:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"method": "notifications/initialized"
+
}
+
```
+
+
- The client **SHOULD NOT** send requests other than
+
[pings]({{< ref "utilities/ping" >}}) before the server has responded to the
+
`initialize` request.
+
- The server **SHOULD NOT** send requests other than
+
[pings]({{< ref "utilities/ping" >}}) and
+
[logging]({{< ref "../server/utilities/logging" >}}) before receiving the `initialized`
+
notification.
+
+
#### Version Negotiation
+
+
In the `initialize` request, the client **MUST** send a protocol version it supports.
+
This **SHOULD** be the _latest_ version supported by the client.
+
+
If the server supports the requested protocol version, it **MUST** respond with the same
+
version. Otherwise, the server **MUST** respond with another protocol version it
+
supports. This **SHOULD** be the _latest_ version supported by the server.
+
+
If the client does not support the version in the server's response, it **SHOULD**
+
disconnect.
+
+
#### Capability Negotiation
+
+
Client and server capabilities establish which optional protocol features will be
+
available during the session.
+
+
Key capabilities include:
+
+
| Category | Capability | Description |
+
| -------- | -------------- | -------------------------------------------------------------------------- |
+
| Client | `roots` | Ability to provide filesystem [roots]({{< ref "../client/roots" >}}) |
+
| Client | `sampling` | Support for LLM [sampling]({{< ref "../client/sampling" >}}) requests |
+
| Client | `experimental` | Describes support for non-standard experimental features |
+
| Server | `prompts` | Offers [prompt templates]({{< ref "../server/prompts" >}}) |
+
| Server | `resources` | Provides readable [resources]({{< ref "../server/resources" >}}) |
+
| Server | `tools` | Exposes callable [tools]({{< ref "../server/tools" >}}) |
+
| Server | `logging` | Emits structured [log messages]({{< ref "../server/utilities/logging" >}}) |
+
| Server | `experimental` | Describes support for non-standard experimental features |
+
+
Capability objects can describe sub-capabilities like:
+
+
- `listChanged`: Support for list change notifications (for prompts, resources, and
+
tools)
+
- `subscribe`: Support for subscribing to individual items' changes (resources only)
+
+
### Operation
+
+
During the operation phase, the client and server exchange messages according to the
+
negotiated capabilities.
+
+
Both parties **SHOULD**:
+
+
- Respect the negotiated protocol version
+
- Only use capabilities that were successfully negotiated
+
+
### Shutdown
+
+
During the shutdown phase, one side (usually the client) cleanly terminates the protocol
+
connection. No specific shutdown messages are definedโ€”instead, the underlying transport
+
mechanism should be used to signal connection termination:
+
+
#### stdio
+
+
For the stdio [transport]({{< ref "transports" >}}), the client **SHOULD** initiate
+
shutdown by:
+
+
1. First, closing the input stream to the child process (the server)
+
2. Waiting for the server to exit, or sending `SIGTERM` if the server does not exit
+
within a reasonable time
+
3. Sending `SIGKILL` if the server does not exit within a reasonable time after `SIGTERM`
+
+
The server **MAY** initiate shutdown by closing its output stream to the client and
+
exiting.
+
+
#### HTTP
+
+
For HTTP [transports]({{< ref "transports" >}}), shutdown is indicated by closing the
+
associated HTTP connection(s).
+
+
## Timeouts
+
+
Implementations **SHOULD** establish timeouts for all sent requests, to prevent hung
+
connections and resource exhaustion. When the request has not received a success or error
+
response within the timeout period, the sender **SHOULD** issue a [cancellation
+
notification]({{< ref "utilities/cancellation" >}}) for that request and stop waiting for
+
a response.
+
+
SDKs and other middleware **SHOULD** allow these timeouts to be configured on a
+
per-request basis.
+
+
Implementations **MAY** choose to reset the timeout clock when receiving a [progress
+
notification]({{< ref "utilities/progress" >}}) corresponding to the request, as this
+
implies that work is actually happening. However, implementations **SHOULD** always
+
enforce a maximum timeout, regardless of progress notifications, to limit the impact of a
+
misbehaving client or server.
+
+
## Error Handling
+
+
Implementations **SHOULD** be prepared to handle these error cases:
+
+
- Protocol version mismatch
+
- Failure to negotiate required capabilities
+
- Request [timeouts](#timeouts)
+
+
Example initialization error:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"error": {
+
"code": -32602,
+
"message": "Unsupported protocol version",
+
"data": {
+
"supported": ["2024-11-05"],
+
"requested": "1.0.0"
+
}
+
}
+
}
+
```
+265
spec/prompts.md
···
+
---
+
title: Prompts
+
weight: 10
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
The Model Context Protocol (MCP) provides a standardized way for servers to expose prompt
+
templates to clients. Prompts allow servers to provide structured messages and
+
instructions for interacting with language models. Clients can discover available
+
prompts, retrieve their contents, and provide arguments to customize them.
+
+
## User Interaction Model
+
+
Prompts are designed to be **user-controlled**, meaning they are exposed from servers to
+
clients with the intention of the user being able to explicitly select them for use.
+
+
Typically, prompts would be triggered through user-initiated commands in the user
+
interface, which allows users to naturally discover and invoke available prompts.
+
+
For example, as slash commands:
+
+
![Example of prompt exposed as slash command](slash-command.png)
+
+
However, implementors are free to expose prompts through any interface pattern that suits
+
their needs&mdash;the protocol itself does not mandate any specific user interaction
+
model.
+
+
## Capabilities
+
+
Servers that support prompts **MUST** declare the `prompts` capability during
+
[initialization]({{< ref "../basic/lifecycle#initialization" >}}):
+
+
/draft`json { "capabilities": { "prompts": { "listChanged": true } } }
+
+
````
+
+
`listChanged` indicates whether the server will emit notifications when the list of
+
available prompts changes.
+
+
## Protocol Messages
+
+
### Listing Prompts
+
+
To retrieve available prompts, clients send a `prompts/list` request. This operation
+
supports [pagination]({{< ref "utilities/pagination" >}}).
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"method": "prompts/list",
+
"params": {
+
"cursor": "optional-cursor-value"
+
}
+
}
+
````
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"result": {
+
"prompts": [
+
{
+
"name": "code_review",
+
"description": "Asks the LLM to analyze code quality and suggest improvements",
+
"arguments": [
+
{
+
"name": "code",
+
"description": "The code to review",
+
"required": true
+
}
+
]
+
}
+
],
+
"nextCursor": "next-page-cursor"
+
}
+
}
+
```
+
+
### Getting a Prompt
+
+
To retrieve a specific prompt, clients send a `prompts/get` request. Arguments may be
+
auto-completed through [the completion API]({{< ref "utilities/completion" >}}).
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"method": "prompts/get",
+
"params": {
+
"name": "code_review",
+
"arguments": {
+
"code": "def hello():\n print('world')"
+
}
+
}
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"result": {
+
"description": "Code review prompt",
+
"messages": [
+
{
+
"role": "user",
+
"content": {
+
"type": "text",
+
"text": "Please review this Python code:\ndef hello():\n print('world')"
+
}
+
}
+
]
+
}
+
}
+
```
+
+
### List Changed Notification
+
+
When the list of available prompts changes, servers that declared the `listChanged`
+
capability **SHOULD** send a notification:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"method": "notifications/prompts/list_changed"
+
}
+
```
+
+
## Message Flow
+
+
```mermaid
+
sequenceDiagram
+
participant Client
+
participant Server
+
+
Note over Client,Server: Discovery
+
Client->>Server: prompts/list
+
Server-->>Client: List of prompts
+
+
Note over Client,Server: Usage
+
Client->>Server: prompts/get
+
Server-->>Client: Prompt content
+
+
opt listChanged
+
Note over Client,Server: Changes
+
Server--)Client: prompts/list_changed
+
Client->>Server: prompts/list
+
Server-->>Client: Updated prompts
+
end
+
```
+
+
## Data Types
+
+
### Prompt
+
+
A prompt definition includes:
+
+
- `name`: Unique identifier for the prompt
+
- `description`: Optional human-readable description
+
- `arguments`: Optional list of arguments for customization
+
+
### PromptMessage
+
+
Messages in a prompt can contain:
+
+
- `role`: Either "user" or "assistant" to indicate the speaker
+
- `content`: One of the following content types:
+
+
#### Text Content
+
+
Text content represents plain text messages:
+
+
```json
+
{
+
"type": "text",
+
"text": "The text content of the message"
+
}
+
```
+
+
This is the most common content type used for natural language interactions.
+
+
#### Image Content
+
+
Image content allows including visual information in messages:
+
+
```json
+
{
+
"type": "image",
+
"data": "base64-encoded-image-data",
+
"mimeType": "image/png"
+
}
+
```
+
+
The image data **MUST** be base64-encoded and include a valid MIME type. This enables
+
multi-modal interactions where visual context is important.
+
+
#### Audio Content
+
+
Audio content allows including audio information in messages:
+
+
```json
+
{
+
"type": "audio",
+
"data": "base64-encoded-audio-data",
+
"mimeType": "audio/wav"
+
}
+
```
+
+
The audio data MUST be base64-encoded and include a valid MIME type. This enables
+
multi-modal interactions where audio context is important.
+
+
#### Embedded Resources
+
+
Embedded resources allow referencing server-side resources directly in messages:
+
+
```json
+
{
+
"type": "resource",
+
"resource": {
+
"uri": "resource://example",
+
"mimeType": "text/plain",
+
"text": "Resource content"
+
}
+
}
+
```
+
+
Resources can contain either text or binary (blob) data and **MUST** include:
+
+
- A valid resource URI
+
- The appropriate MIME type
+
- Either text content or base64-encoded blob data
+
+
Embedded resources enable prompts to seamlessly incorporate server-managed content like
+
documentation, code samples, or other reference materials directly into the conversation
+
flow.
+
+
## Error Handling
+
+
Servers **SHOULD** return standard JSON-RPC errors for common failure cases:
+
+
- Invalid prompt name: `-32602` (Invalid params)
+
- Missing required arguments: `-32602` (Invalid params)
+
- Internal errors: `-32603` (Internal error)
+
+
## Implementation Considerations
+
+
1. Servers **SHOULD** validate prompt arguments before processing
+
2. Clients **SHOULD** handle pagination for large prompt lists
+
3. Both parties **SHOULD** respect capability negotiation
+
+
## Security
+
+
Implementations **MUST** carefully validate all prompt inputs and outputs to prevent
+
injection attacks or unauthorized access to resources.
+357
spec/resources.md
···
+
---
+
title: Resources
+
type: docs
+
weight: 20
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
The Model Context Protocol (MCP) provides a standardized way for servers to expose
+
resources to clients. Resources allow servers to share data that provides context to
+
language models, such as files, database schemas, or application-specific information.
+
Each resource is uniquely identified by a
+
[URI](https://datatracker.ietf.org/doc/html/rfc3986).
+
+
## User Interaction Model
+
+
Resources in MCP are designed to be **application-driven**, with host applications
+
determining how to incorporate context based on their needs.
+
+
For example, applications could:
+
+
- Expose resources through UI elements for explicit selection, in a tree or list view
+
- Allow the user to search through and filter available resources
+
- Implement automatic context inclusion, based on heuristics or the AI model's selection
+
+
![Example of resource context picker](resource-picker.png)
+
+
However, implementations are free to expose resources through any interface pattern that
+
suits their needs&mdash;the protocol itself does not mandate any specific user
+
interaction model.
+
+
## Capabilities
+
+
Servers that support resources **MUST** declare the `resources` capability:
+
+
```json
+
{
+
"capabilities": {
+
"resources": {
+
"subscribe": true,
+
"listChanged": true
+
}
+
}
+
}
+
```
+
+
The capability supports two optional features:
+
+
- `subscribe`: whether the client can subscribe to be notified of changes to individual
+
resources.
+
- `listChanged`: whether the server will emit notifications when the list of available
+
resources changes.
+
+
Both `subscribe` and `listChanged` are optional&mdash;servers can support neither,
+
either, or both:
+
+
```json
+
{
+
"capabilities": {
+
"resources": {} // Neither feature supported
+
}
+
}
+
```
+
+
```json
+
{
+
"capabilities": {
+
"resources": {
+
"subscribe": true // Only subscriptions supported
+
}
+
}
+
}
+
```
+
+
```json
+
{
+
"capabilities": {
+
"resources": {
+
"listChanged": true // Only list change notifications supported
+
}
+
}
+
}
+
```
+
+
## Protocol Messages
+
+
### Listing Resources
+
+
To discover available resources, clients send a `resources/list` request. This operation
+
supports [pagination]({{< ref "utilities/pagination" >}}).
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"method": "resources/list",
+
"params": {
+
"cursor": "optional-cursor-value"
+
}
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"result": {
+
"resources": [
+
{
+
"uri": "file:///project/src/main.rs",
+
"name": "main.rs",
+
"description": "Primary application entry point",
+
"mimeType": "text/x-rust"
+
}
+
],
+
"nextCursor": "next-page-cursor"
+
}
+
}
+
```
+
+
### Reading Resources
+
+
To retrieve resource contents, clients send a `resources/read` request:
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"method": "resources/read",
+
"params": {
+
"uri": "file:///project/src/main.rs"
+
}
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"result": {
+
"contents": [
+
{
+
"uri": "file:///project/src/main.rs",
+
"mimeType": "text/x-rust",
+
"text": "fn main() {\n println!(\"Hello world!\");\n}"
+
}
+
]
+
}
+
}
+
```
+
+
### Resource Templates
+
+
Resource templates allow servers to expose parameterized resources using
+
[URI templates](https://datatracker.ietf.org/doc/html/rfc6570). Arguments may be
+
auto-completed through [the completion API]({{< ref "utilities/completion" >}}).
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 3,
+
"method": "resources/templates/list"
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 3,
+
"result": {
+
"resourceTemplates": [
+
{
+
"uriTemplate": "file:///{path}",
+
"name": "Project Files",
+
"description": "Access files in the project directory",
+
"mimeType": "application/octet-stream"
+
}
+
]
+
}
+
}
+
```
+
+
### List Changed Notification
+
+
When the list of available resources changes, servers that declared the `listChanged`
+
capability **SHOULD** send a notification:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"method": "notifications/resources/list_changed"
+
}
+
```
+
+
### Subscriptions
+
+
The protocol supports optional subscriptions to resource changes. Clients can subscribe
+
to specific resources and receive notifications when they change:
+
+
**Subscribe Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 4,
+
"method": "resources/subscribe",
+
"params": {
+
"uri": "file:///project/src/main.rs"
+
}
+
}
+
```
+
+
**Update Notification:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"method": "notifications/resources/updated",
+
"params": {
+
"uri": "file:///project/src/main.rs"
+
}
+
}
+
```
+
+
## Message Flow
+
+
```mermaid
+
sequenceDiagram
+
participant Client
+
participant Server
+
+
Note over Client,Server: Resource Discovery
+
Client->>Server: resources/list
+
Server-->>Client: List of resources
+
+
Note over Client,Server: Resource Access
+
Client->>Server: resources/read
+
Server-->>Client: Resource contents
+
+
Note over Client,Server: Subscriptions
+
Client->>Server: resources/subscribe
+
Server-->>Client: Subscription confirmed
+
+
Note over Client,Server: Updates
+
Server--)Client: notifications/resources/updated
+
Client->>Server: resources/read
+
Server-->>Client: Updated contents
+
```
+
+
## Data Types
+
+
### Resource
+
+
A resource definition includes:
+
+
- `uri`: Unique identifier for the resource
+
- `name`: Human-readable name
+
- `description`: Optional description
+
- `mimeType`: Optional MIME type
+
- `size`: Optional size in bytes
+
+
### Resource Contents
+
+
Resources can contain either text or binary data:
+
+
#### Text Content
+
+
```json
+
{
+
"uri": "file:///example.txt",
+
"mimeType": "text/plain",
+
"text": "Resource content"
+
}
+
```
+
+
#### Binary Content
+
+
```json
+
{
+
"uri": "file:///example.png",
+
"mimeType": "image/png",
+
"blob": "base64-encoded-data"
+
}
+
```
+
+
## Common URI Schemes
+
+
The protocol defines several standard URI schemes. This list not
+
exhaustive&mdash;implementations are always free to use additional, custom URI schemes.
+
+
### https://
+
+
Used to represent a resource available on the web.
+
+
Servers **SHOULD** use this scheme only when the client is able to fetch and load the
+
resource directly from the web on its ownโ€”that is, it doesnโ€™t need to read the resource
+
via the MCP server.
+
+
For other use cases, servers **SHOULD** prefer to use another URI scheme, or define a
+
custom one, even if the server will itself be downloading resource contents over the
+
internet.
+
+
### file://
+
+
Used to identify resources that behave like a filesystem. However, the resources do not
+
need to map to an actual physical filesystem.
+
+
MCP servers **MAY** identify file:// resources with an
+
[XDG MIME type](https://specifications.freedesktop.org/shared-mime-info-spec/0.14/ar01s02.html#id-1.3.14),
+
like `inode/directory`, to represent non-regular files (such as directories) that donโ€™t
+
otherwise have a standard MIME type.
+
+
### git://
+
+
Git version control integration.
+
+
## Error Handling
+
+
Servers **SHOULD** return standard JSON-RPC errors for common failure cases:
+
+
- Resource not found: `-32002`
+
- Internal errors: `-32603`
+
+
Example error:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 5,
+
"error": {
+
"code": -32002,
+
"message": "Resource not found",
+
"data": {
+
"uri": "file:///nonexistent.txt"
+
}
+
}
+
}
+
```
+
+
## Security Considerations
+
+
1. Servers **MUST** validate all resource URIs
+
2. Access controls **SHOULD** be implemented for sensitive resources
+
3. Binary data **MUST** be properly encoded
+
4. Resource permissions **SHOULD** be checked before operations
spec/slash-command.png

This is a binary file and will not be displayed.

+299
spec/tools.md
···
+
---
+
title: Tools
+
type: docs
+
weight: 40
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
The Model Context Protocol (MCP) allows servers to expose tools that can be invoked by
+
language models. Tools enable models to interact with external systems, such as querying
+
databases, calling APIs, or performing computations. Each tool is uniquely identified by
+
a name and includes metadata describing its schema.
+
+
## User Interaction Model
+
+
Tools in MCP are designed to be **model-controlled**, meaning that the language model can
+
discover and invoke tools automatically based on its contextual understanding and the
+
user's prompts.
+
+
However, implementations are free to expose tools through any interface pattern that
+
suits their needs&mdash;the protocol itself does not mandate any specific user
+
interaction model.
+
+
{{< callout type="warning" >}} For trust & safety and security, there **SHOULD** always
+
be a human in the loop with the ability to deny tool invocations.
+
+
Applications **SHOULD**:
+
+
- Provide UI that makes clear which tools are being exposed to the AI model
+
- Insert clear visual indicators when tools are invoked
+
- Present confirmation prompts to the user for operations, to ensure a human is in the
+
loop {{< /callout >}}
+
+
## Capabilities
+
+
Servers that support tools **MUST** declare the `tools` capability:
+
+
```json
+
{
+
"capabilities": {
+
"tools": {
+
"listChanged": true
+
}
+
}
+
}
+
```
+
+
`listChanged` indicates whether the server will emit notifications when the list of
+
available tools changes.
+
+
## Protocol Messages
+
+
### Listing Tools
+
+
To discover available tools, clients send a `tools/list` request. This operation supports
+
[pagination]({{< ref "utilities/pagination" >}}).
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"method": "tools/list",
+
"params": {
+
"cursor": "optional-cursor-value"
+
}
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 1,
+
"result": {
+
"tools": [
+
{
+
"name": "get_weather",
+
"description": "Get current weather information for a location",
+
"inputSchema": {
+
"type": "object",
+
"properties": {
+
"location": {
+
"type": "string",
+
"description": "City name or zip code"
+
}
+
},
+
"required": ["location"]
+
}
+
}
+
],
+
"nextCursor": "next-page-cursor"
+
}
+
}
+
```
+
+
### Calling Tools
+
+
To invoke a tool, clients send a `tools/call` request:
+
+
**Request:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"method": "tools/call",
+
"params": {
+
"name": "get_weather",
+
"arguments": {
+
"location": "New York"
+
}
+
}
+
}
+
```
+
+
**Response:**
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 2,
+
"result": {
+
"content": [
+
{
+
"type": "text",
+
"text": "Current weather in New York:\nTemperature: 72ยฐF\nConditions: Partly cloudy"
+
}
+
],
+
"isError": false
+
}
+
}
+
```
+
+
### List Changed Notification
+
+
When the list of available tools changes, servers that declared the `listChanged`
+
capability **SHOULD** send a notification:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"method": "notifications/tools/list_changed"
+
}
+
```
+
+
## Message Flow
+
+
```mermaid
+
sequenceDiagram
+
participant LLM
+
participant Client
+
participant Server
+
+
Note over Client,Server: Discovery
+
Client->>Server: tools/list
+
Server-->>Client: List of tools
+
+
Note over Client,LLM: Tool Selection
+
LLM->>Client: Select tool to use
+
+
Note over Client,Server: Invocation
+
Client->>Server: tools/call
+
Server-->>Client: Tool result
+
Client->>LLM: Process result
+
+
Note over Client,Server: Updates
+
Server--)Client: tools/list_changed
+
Client->>Server: tools/list
+
Server-->>Client: Updated tools
+
```
+
+
## Data Types
+
+
### Tool
+
+
A tool definition includes:
+
+
- `name`: Unique identifier for the tool
+
- `description`: Human-readable description of functionality
+
- `inputSchema`: JSON Schema defining expected parameters
+
- `annotations`: optional properties describing tool behavior
+
+
{{< callout type="warning" >}} For trust & safety and security, clients **MUST** consider
+
tool annotations to be untrusted unless they come from trusted servers. {{< /callout >}}
+
+
### Tool Result
+
+
Tool results can contain multiple content items of different types:
+
+
#### Text Content
+
+
```json
+
{
+
"type": "text",
+
"text": "Tool result text"
+
}
+
```
+
+
#### Image Content
+
+
```json
+
{
+
"type": "image",
+
"data": "base64-encoded-data",
+
"mimeType": "image/png"
+
}
+
```
+
+
#### Audio Content
+
+
```json
+
{
+
"type": "audio",
+
"data": "base64-encoded-audio-data",
+
"mimeType": "audio/wav"
+
}
+
```
+
+
#### Embedded Resources
+
+
[Resources]({{< ref "resources" >}}) **MAY** be embedded, to provide additional context
+
or data, behind a URI that can be subscribed to or fetched again by the client later:
+
+
```json
+
{
+
"type": "resource",
+
"resource": {
+
"uri": "resource://example",
+
"mimeType": "text/plain",
+
"text": "Resource content"
+
}
+
}
+
```
+
+
## Error Handling
+
+
Tools use two error reporting mechanisms:
+
+
1. **Protocol Errors**: Standard JSON-RPC errors for issues like:
+
+
- Unknown tools
+
- Invalid arguments
+
- Server errors
+
+
2. **Tool Execution Errors**: Reported in tool results with `isError: true`:
+
- API failures
+
- Invalid input data
+
- Business logic errors
+
+
Example protocol error:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 3,
+
"error": {
+
"code": -32602,
+
"message": "Unknown tool: invalid_tool_name"
+
}
+
}
+
```
+
+
Example tool execution error:
+
+
```json
+
{
+
"jsonrpc": "2.0",
+
"id": 4,
+
"result": {
+
"content": [
+
{
+
"type": "text",
+
"text": "Failed to fetch weather data: API rate limit exceeded"
+
}
+
],
+
"isError": true
+
}
+
}
+
```
+
+
## Security Considerations
+
+
1. Servers **MUST**:
+
+
- Validate all tool inputs
+
- Implement proper access controls
+
- Rate limit tool invocations
+
- Sanitize tool outputs
+
+
2. Clients **SHOULD**:
+
- Prompt for user confirmation on sensitive operations
+
- Show tool inputs to the user before calling the server, to avoid malicious or
+
accidental data exfiltration
+
- Validate tool results before passing to LLM
+
- Implement timeouts for tool calls
+
- Log tool usage for audit purposes
+278
spec/transports.md
···
+
---
+
title: Transports
+
type: docs
+
weight: 10
+
---
+
+
{{< callout type="info" >}} **Protocol Revision**: 2025-03-26 {{< /callout >}}
+
+
MCP uses JSON-RPC to encode messages. JSON-RPC messages **MUST** be UTF-8 encoded.
+
+
The protocol currently defines two standard transport mechanisms for client-server
+
communication:
+
+
1. [stdio](#stdio), communication over standard in and standard out
+
2. [Streamable HTTP](#streamable-http)
+
+
Clients **SHOULD** support stdio whenever possible.
+
+
It is also possible for clients and servers to implement
+
[custom transports](#custom-transports) in a pluggable fashion.
+
+
## stdio
+
+
In the **stdio** transport:
+
+
- The client launches the MCP server as a subprocess.
+
- The server reads JSON-RPC messages from its standard input (`stdin`) and sends messages
+
to its standard output (`stdout`).
+
- Messages may be JSON-RPC requests, notifications, responsesโ€”or a JSON-RPC
+
[batch](https://www.jsonrpc.org/specification#batch) containing one or more requests
+
and/or notifications.
+
- Messages are delimited by newlines, and **MUST NOT** contain embedded newlines.
+
- The server **MAY** write UTF-8 strings to its standard error (`stderr`) for logging
+
purposes. Clients **MAY** capture, forward, or ignore this logging.
+
- The server **MUST NOT** write anything to its `stdout` that is not a valid MCP message.
+
- The client **MUST NOT** write anything to the server's `stdin` that is not a valid MCP
+
message.
+
+
```mermaid
+
sequenceDiagram
+
participant Client
+
participant Server Process
+
+
Client->>+Server Process: Launch subprocess
+
loop Message Exchange
+
Client->>Server Process: Write to stdin
+
Server Process->>Client: Write to stdout
+
Server Process--)Client: Optional logs on stderr
+
end
+
Client->>Server Process: Close stdin, terminate subprocess
+
deactivate Server Process
+
```
+
+
## Streamable HTTP
+
+
{{< callout type="info" >}} This replaces the [HTTP+SSE
+
transport]({{< ref "/specification/2024-11-05/basic/transports#http-with-sse" >}}) from
+
protocol version 2024-11-05. See the [backwards compatibility](#backwards-compatibility)
+
guide below. {{< /callout >}}
+
+
In the **Streamable HTTP** transport, the server operates as an independent process that
+
can handle multiple client connections. This transport uses HTTP POST and GET requests.
+
Server can optionally make use of
+
[Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) (SSE) to stream
+
multiple server messages. This permits basic MCP servers, as well as more feature-rich
+
servers supporting streaming and server-to-client notifications and requests.
+
+
The server **MUST** provide a single HTTP endpoint path (hereafter referred to as the
+
**MCP endpoint**) that supports both POST and GET methods. For example, this could be a
+
URL like `https://example.com/mcp`.
+
+
### Sending Messages to the Server
+
+
Every JSON-RPC message sent from the client **MUST** be a new HTTP POST request to the
+
MCP endpoint.
+
+
1. The client **MUST** use HTTP POST to send JSON-RPC messages to the MCP endpoint.
+
2. The client **MUST** include an `Accept` header, listing both `application/json` and
+
`text/event-stream` as supported content types.
+
3. The body of the POST request **MUST** be one of the following:
+
- A single JSON-RPC _request_, _notification_, or _response_
+
- An array [batching](https://www.jsonrpc.org/specification#batch) one or more
+
_requests and/or notifications_
+
- An array [batching](https://www.jsonrpc.org/specification#batch) one or more
+
_responses_
+
4. If the input consists solely of (any number of) JSON-RPC _responses_ or
+
_notifications_:
+
- If the server accepts the input, the server **MUST** return HTTP status code 202
+
Accepted with no body.
+
- If the server cannot accept the input, it **MUST** return an HTTP error status code
+
(e.g., 400 Bad Request). The HTTP response body **MAY** comprise a JSON-RPC _error
+
response_ that has no `id`.
+
5. If the input contains any number of JSON-RPC _requests_, the server **MUST** either
+
return `Content-Type: text/event-stream`, to initiate an SSE stream, or
+
`Content-Type: application/json`, to return one JSON object. The client **MUST**
+
support both these cases.
+
6. If the server initiates an SSE stream:
+
- The SSE stream **SHOULD** eventually include one JSON-RPC _response_ per each
+
JSON-RPC _request_ sent in the POST body. These _responses_ **MAY** be
+
[batched](https://www.jsonrpc.org/specification#batch).
+
- The server **MAY** send JSON-RPC _requests_ and _notifications_ before sending a
+
JSON-RPC _response_. These messages **SHOULD** relate to the originating client
+
_request_. These _requests_ and _notifications_ **MAY** be
+
[batched](https://www.jsonrpc.org/specification#batch).
+
- The server **SHOULD NOT** close the SSE stream before sending a JSON-RPC _response_
+
per each received JSON-RPC _request_, unless the [session](#session-management)
+
expires.
+
- After all JSON-RPC _responses_ have been sent, the server **SHOULD** close the SSE
+
stream.
+
- Disconnection **MAY** occur at any time (e.g., due to network conditions).
+
Therefore:
+
- Disconnection **SHOULD NOT** be interpreted as the client cancelling its request.
+
- To cancel, the client **SHOULD** explicitly send an MCP `CancelledNotification`.
+
- To avoid message loss due to disconnection, the server **MAY** make the stream
+
[resumable](#resumability-and-redelivery).
+
+
### Listening for Messages from the Server
+
+
1. The client **MAY** issue an HTTP GET to the MCP endpoint. This can be used to open an
+
SSE stream, allowing the server to communicate to the client, without the client first
+
sending data via HTTP POST.
+
2. The client **MUST** include an `Accept` header, listing `text/event-stream` as a
+
supported content type.
+
3. The server **MUST** either return `Content-Type: text/event-stream` in response to
+
this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that the server
+
does not offer an SSE stream at this endpoint.
+
4. If the server initiates an SSE stream:
+
- The server **MAY** send JSON-RPC _requests_ and _notifications_ on the stream. These
+
_requests_ and _notifications_ **MAY** be
+
[batched](https://www.jsonrpc.org/specification#batch).
+
- These messages **SHOULD** be unrelated to any concurrently-running JSON-RPC
+
_request_ from the client.
+
- The server **MUST NOT** send a JSON-RPC _response_ on the stream **unless**
+
[resuming](#resumability-and-redelivery) a stream associated with a previous client
+
request.
+
- The server **MAY** close the SSE stream at any time.
+
- The client **MAY** close the SSE stream at any time.
+
+
### Multiple Connections
+
+
1. The client **MAY** remain connected to multiple SSE streams simultaneously.
+
2. The server **MUST** send each of its JSON-RPC messages on only one of the connected
+
streams; that is, it **MUST NOT** broadcast the same message across multiple streams.
+
- The risk of message loss **MAY** be mitigated by making the stream
+
[resumable](#resumability-and-redelivery).
+
+
### Resumability and Redelivery
+
+
To support resuming broken connections, and redelivering messages that might otherwise be
+
lost:
+
+
1. Servers **MAY** attach an `id` field to their SSE events, as described in the
+
[SSE standard](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation).
+
- If present, the ID **MUST** be globally unique across all streams within that
+
[session](#session-management)โ€”or all streams with that specific client, if session
+
management is not in use.
+
2. If the client wishes to resume after a broken connection, it **SHOULD** issue an HTTP
+
GET to the MCP endpoint, and include the
+
[`Last-Event-ID`](https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header)
+
header to indicate the last event ID it received.
+
- The server **MAY** use this header to replay messages that would have been sent
+
after the last event ID, _on the stream that was disconnected_, and to resume the
+
stream from that point.
+
- The server **MUST NOT** replay messages that would have been delivered on a
+
different stream.
+
+
In other words, these event IDs should be assigned by servers on a _per-stream_ basis, to
+
act as a cursor within that particular stream.
+
+
### Session Management
+
+
An MCP "session" consists of logically related interactions between a client and a
+
server, beginning with the [initialization phase]({{< ref "lifecycle" >}}). To support
+
servers which want to establish stateful sessions:
+
+
1. A server using the Streamable HTTP transport **MAY** assign a session ID at
+
initialization time, by including it in an `Mcp-Session-Id` header on the HTTP
+
response containing the `InitializeResult`.
+
- The session ID **SHOULD** be globally unique and cryptographically secure (e.g., a
+
securely generated UUID, a JWT, or a cryptographic hash).
+
- The session ID **MUST** only contain visible ASCII characters (ranging from 0x21 to
+
0x7E).
+
2. If an `Mcp-Session-Id` is returned by the server during initialization, clients using
+
the Streamable HTTP transport **MUST** include it in the `Mcp-Session-Id` header on
+
all of their subsequent HTTP requests.
+
- Servers that require a session ID **SHOULD** respond to requests without an
+
`Mcp-Session-Id` header (other than initialization) with HTTP 400 Bad Request.
+
3. The server **MAY** terminate the session at any time, after which it **MUST** respond
+
to requests containing that session ID with HTTP 404 Not Found.
+
4. When a client receives HTTP 404 in response to a request containing an
+
`Mcp-Session-Id`, it **MUST** start a new session by sending a new `InitializeRequest`
+
without a session ID attached.
+
5. Clients that no longer need a particular session (e.g., because the user is leaving
+
the client application) **SHOULD** send an HTTP DELETE to the MCP endpoint with the
+
`Mcp-Session-Id` header, to explicitly terminate the session.
+
- The server **MAY** respond to this request with HTTP 405 Method Not Allowed,
+
indicating that the server does not allow clients to terminate sessions.
+
+
### Sequence Diagram
+
+
```mermaid
+
sequenceDiagram
+
participant Client
+
participant Server
+
+
note over Client, Server: initialization
+
+
Client->>+Server: POST InitializeRequest
+
Server->>-Client: InitializeResponse<br>Mcp-Session-Id: 1868a90c...
+
+
Client->>+Server: POST InitializedNotification<br>Mcp-Session-Id: 1868a90c...
+
Server->>-Client: 202 Accepted
+
+
note over Client, Server: client requests
+
Client->>+Server: POST ... request ...<br>Mcp-Session-Id: 1868a90c...
+
+
alt single HTTP response
+
Server->>Client: ... response ...
+
else server opens SSE stream
+
loop while connection remains open
+
Server-)Client: ... SSE messages from server ...
+
end
+
Server-)Client: SSE event: ... response ...
+
end
+
deactivate Server
+
+
note over Client, Server: client notifications/responses
+
Client->>+Server: POST ... notification/response ...<br>Mcp-Session-Id: 1868a90c...
+
Server->>-Client: 202 Accepted
+
+
note over Client, Server: server requests
+
Client->>+Server: GET<br>Mcp-Session-Id: 1868a90c...
+
loop while connection remains open
+
Server-)Client: ... SSE messages from server ...
+
end
+
deactivate Server
+
+
```
+
+
### Backwards Compatibility
+
+
Clients and servers can maintain backwards compatibility with the deprecated [HTTP+SSE
+
transport]({{< ref "/specification/2024-11-05/basic/transports#http-with-sse" >}}) (from
+
protocol version 2024-11-05) as follows:
+
+
**Servers** wanting to support older clients should:
+
+
- Continue to host both the SSE and POST endpoints of the old transport, alongside the
+
new "MCP endpoint" defined for the Streamable HTTP transport.
+
- It is also possible to combine the old POST endpoint and the new MCP endpoint, but
+
this may introduce unneeded complexity.
+
+
**Clients** wanting to support older servers should:
+
+
1. Accept an MCP server URL from the user, which may point to either a server using the
+
old transport or the new transport.
+
2. Attempt to POST an `InitializeRequest` to the server URL, with an `Accept` header as
+
defined above:
+
- If it succeeds, the client can assume this is a server supporting the new Streamable
+
HTTP transport.
+
- If it fails with an HTTP 4xx status code (e.g., 405 Method Not Allowed or 404 Not
+
Found):
+
- Issue a GET request to the server URL, expecting that this will open an SSE stream
+
and return an `endpoint` event as the first event.
+
- When the `endpoint` event arrives, the client can assume this is a server running
+
the old HTTP+SSE transport, and should use that transport for all subsequent
+
communication.
+
+
## Custom Transports
+
+
Clients and servers **MAY** implement additional custom transport mechanisms to suit
+
their specific needs. The protocol is transport-agnostic and can be implemented over any
+
communication channel that supports bidirectional message exchange.
+
+
Implementers who choose to support custom transports **MUST** ensure they preserve the
+
JSON-RPC message format and lifecycle requirements defined by MCP. Custom transports
+
**SHOULD** document their specific connection establishment and message exchange patterns
+
to aid interoperability.