Model Context Protocol in OCaml

add MCP capitalize server implementation

Implements a simple MCP server that exposes a 'capitalize' tool which converts text to uppercase.
Includes comprehensive debugging and fixes protocol compatibility issues for Claude Desktop.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+368
bin
+3
bin/dune
···
+
(executable
+
(name server)
+
(libraries mcp yojson unix))
+365
bin/server.ml
···
+
open Mcp
+
open Jsonrpc
+
+
(* Logging utilities *)
+
let log_debug msg =
+
Printf.eprintf "[DEBUG] %s\n" msg;
+
flush stderr
+
+
let log_error msg =
+
Printf.eprintf "[ERROR] %s\n" msg;
+
flush stderr
+
+
(* Server state *)
+
let protocol_version = "2024-11-05"
+
let server_info = Implementation.{ name = "ocaml-mcp-capitalizer"; version = "0.1.0" }
+
let server_capabilities = `Assoc [
+
(* We support tools *)
+
("tools", `Assoc [
+
("listChanged", `Bool true)
+
]);
+
(* We don't support resources - make this explicit *)
+
("resources", `Assoc [
+
("listChanged", `Bool false);
+
("subscribe", `Bool false)
+
]);
+
(* We don't support prompts - make this explicit *)
+
("prompts", `Assoc [
+
("listChanged", `Bool false)
+
])
+
]
+
+
(* Tool implementation *)
+
module CapitalizeTool = struct
+
let name = "capitalize"
+
let description = "Capitalizes the provided text"
+
let input_schema = `Assoc [
+
("type", `String "object");
+
("properties", `Assoc [
+
("text", `Assoc [
+
("type", `String "string");
+
("description", `String "The text to capitalize")
+
])
+
]);
+
("required", `List [`String "text"])
+
]
+
+
let call json =
+
match json with
+
| `Assoc fields ->
+
(match List.assoc_opt "text" fields with
+
| Some (`String text) ->
+
let capitalized_text = String.uppercase_ascii text in
+
let content = TextContent.{
+
text = capitalized_text;
+
annotations = None
+
} in
+
`Assoc [
+
("content", `List [TextContent.yojson_of_t content]);
+
("isError", `Bool false)
+
]
+
| _ ->
+
let error_content = TextContent.{
+
text = "Missing or invalid 'text' parameter";
+
annotations = None
+
} in
+
`Assoc [
+
("content", `List [TextContent.yojson_of_t error_content]);
+
("isError", `Bool true)
+
])
+
| _ ->
+
let error_content = TextContent.{
+
text = "Invalid arguments format";
+
annotations = None
+
} in
+
`Assoc [
+
("content", `List [TextContent.yojson_of_t error_content]);
+
("isError", `Bool true)
+
]
+
end
+
+
(* Handle tool listing *)
+
let list_tools () =
+
let tool = `Assoc [
+
("name", `String CapitalizeTool.name);
+
("description", `String CapitalizeTool.description);
+
("inputSchema", CapitalizeTool.input_schema)
+
] in
+
`Assoc [
+
("tools", `List [tool])
+
]
+
+
(* Handle tool calls *)
+
let call_tool name args =
+
if name = CapitalizeTool.name then
+
CapitalizeTool.call args
+
else
+
let error_content = TextContent.{
+
text = Printf.sprintf "Unknown tool: %s" name;
+
annotations = None
+
} in
+
`Assoc [
+
("content", `List [TextContent.yojson_of_t error_content]);
+
("isError", `Bool true)
+
]
+
+
(* Handle initialization *)
+
let handle_initialize id json =
+
try
+
log_debug (Printf.sprintf "Processing initialize request with id: %s"
+
(match id with
+
| `Int i -> string_of_int i
+
| `String s -> s));
+
+
log_debug (Printf.sprintf "Initialize params: %s"
+
(match json with
+
| Some j -> Yojson.Safe.to_string j
+
| None -> "null"));
+
+
let _ = match json with
+
| Some params ->
+
log_debug "Parsing initialize request params...";
+
let req = Initialize.Request.t_of_yojson params in
+
log_debug (Printf.sprintf "Client info: %s v%s" req.client_info.name req.client_info.version);
+
log_debug (Printf.sprintf "Client protocol version: %s" req.protocol_version);
+
+
(* Check protocol version compatibility *)
+
if req.protocol_version <> protocol_version then
+
log_debug (Printf.sprintf "Protocol version mismatch: client=%s server=%s - will use server version"
+
req.protocol_version protocol_version);
+
+
req
+
| None ->
+
log_error "Missing params for initialize request";
+
raise (Json.Of_json ("Missing params for initialize request", `Null))
+
in
+
+
log_debug "Creating initialize response...";
+
let result = Initialize.Result.create
+
~capabilities:server_capabilities
+
~server_info
+
~protocol_version
+
~instructions:"This server provides a tool to capitalize text."
+
()
+
in
+
+
log_debug "Serializing initialize response...";
+
let response = create_response ~id ~result:(Initialize.Result.yojson_of_t result) in
+
log_debug "Initialize response created successfully";
+
response
+
with
+
| Json.Of_json (msg, _) ->
+
log_error (Printf.sprintf "JSON error in initialize: %s" msg);
+
create_error ~id ~code:(-32602) ~message:("Invalid params: " ^ msg) ()
+
| exc ->
+
log_error (Printf.sprintf "Exception in initialize: %s" (Printexc.to_string exc));
+
log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
create_error ~id ~code:(-32603) ~message:("Internal error: " ^ (Printexc.to_string exc)) ()
+
+
(* Handle tools/list *)
+
let handle_list_tools id =
+
log_debug "Processing tools/list request";
+
let result = list_tools () in
+
log_debug (Printf.sprintf "Tools list result: %s" (Yojson.Safe.to_string result));
+
create_response ~id ~result
+
+
(* Handle tools/call *)
+
let handle_call_tool id json =
+
try
+
log_debug (Printf.sprintf "Processing tool call request with id: %s"
+
(match id with
+
| `Int i -> string_of_int i
+
| `String s -> s));
+
+
log_debug (Printf.sprintf "Tool call params: %s"
+
(match json with
+
| Some j -> Yojson.Safe.to_string j
+
| None -> "null"));
+
+
match json with
+
| Some (`Assoc params) ->
+
let name = match List.assoc_opt "name" params with
+
| Some (`String name) ->
+
log_debug (Printf.sprintf "Tool name: %s" name);
+
name
+
| _ ->
+
log_error "Missing or invalid 'name' parameter in tool call";
+
raise (Json.Of_json ("Missing or invalid 'name' parameter", `Assoc params))
+
in
+
let args = match List.assoc_opt "arguments" params with
+
| Some (args) ->
+
log_debug (Printf.sprintf "Tool arguments: %s" (Yojson.Safe.to_string args));
+
args
+
| _ ->
+
log_debug "No arguments provided for tool call, using empty object";
+
`Assoc [] (* Empty arguments is valid *)
+
in
+
log_debug (Printf.sprintf "Calling tool: %s" name);
+
let result = call_tool name args in
+
log_debug (Printf.sprintf "Tool call result: %s" (Yojson.Safe.to_string result));
+
create_response ~id ~result
+
| _ ->
+
log_error "Invalid params format for tools/call";
+
create_error ~id ~code:(-32602) ~message:"Invalid params for tools/call" ()
+
with
+
| Json.Of_json (msg, _) ->
+
log_error (Printf.sprintf "JSON error in tool call: %s" msg);
+
create_error ~id ~code:(-32602) ~message:("Invalid params: " ^ msg) ()
+
| exc ->
+
log_error (Printf.sprintf "Exception in tool call: %s" (Printexc.to_string exc));
+
log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
create_error ~id ~code:(-32603) ~message:("Internal error: " ^ (Printexc.to_string exc)) ()
+
+
(* Handle ping *)
+
let handle_ping id =
+
create_response ~id ~result:(`Assoc [])
+
+
(* Process a single message *)
+
let process_message message =
+
try
+
log_debug "Parsing message as JSONRPC message...";
+
match JSONRPCMessage.t_of_yojson message with
+
| JSONRPCMessage.Request req ->
+
log_debug (Printf.sprintf "Received request with method: %s" req.method_);
+
(match req.method_ with
+
| "initialize" ->
+
log_debug "Processing initialize request";
+
Some (handle_initialize req.id req.params)
+
| "tools/list" ->
+
log_debug "Processing tools/list request";
+
Some (handle_list_tools req.id)
+
| "tools/call" ->
+
log_debug "Processing tools/call request";
+
Some (handle_call_tool req.id req.params)
+
| "ping" ->
+
log_debug "Processing ping request";
+
Some (handle_ping req.id)
+
| _ ->
+
log_error (Printf.sprintf "Unknown method received: %s" req.method_);
+
Some (create_error ~id:req.id ~code:(-32601) ~message:("Method not found: " ^ req.method_) ()))
+
| JSONRPCMessage.Notification notif ->
+
log_debug (Printf.sprintf "Received notification with method: %s" notif.method_);
+
(match notif.method_ with
+
| "notifications/initialized" ->
+
log_debug "Client initialization complete - Server is now ready to receive requests";
+
log_debug (Printf.sprintf "Notification params: %s"
+
(match notif.params with
+
| Some p -> Yojson.Safe.to_string p
+
| None -> "null"));
+
None
+
| _ ->
+
log_debug (Printf.sprintf "Ignoring notification: %s" notif.method_);
+
None)
+
| JSONRPCMessage.Response _ ->
+
log_error "Unexpected response message received";
+
None
+
| JSONRPCMessage.Error _ ->
+
log_error "Unexpected error message received";
+
None
+
with
+
| exc ->
+
log_error (Printf.sprintf "Exception during message processing: %s" (Printexc.to_string exc));
+
log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
+
log_error (Printf.sprintf "Message was: %s" (Yojson.Safe.to_string message));
+
None
+
+
(* Main loop *)
+
let rec read_message () =
+
try
+
log_debug "Attempting to read line from stdin...";
+
let line = read_line () in
+
if line = "" then (
+
log_debug "Empty line received, ignoring";
+
None
+
) else (
+
log_debug (Printf.sprintf "Raw input: %s" line);
+
try
+
let json = Yojson.Safe.from_string line in
+
log_debug "Successfully parsed JSON";
+
Some json
+
with
+
| Yojson.Json_error msg ->
+
log_error (Printf.sprintf "Error parsing JSON: %s" msg);
+
log_error (Printf.sprintf "Input was: %s" line);
+
read_message ()
+
)
+
with
+
| End_of_file ->
+
log_debug "End of file received on stdin";
+
None
+
| Sys_error msg ->
+
log_error (Printf.sprintf "System error while reading: %s" msg);
+
None
+
| exc ->
+
log_error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exc));
+
None
+
+
let () =
+
try
+
(* Enable exception backtraces *)
+
Printexc.record_backtrace true;
+
+
(* Enable line buffering for stdout *)
+
set_binary_mode_out stdout false;
+
+
log_debug "MCP Capitalizer server started";
+
log_debug (Printf.sprintf "Protocol version: %s" protocol_version);
+
log_debug (Printf.sprintf "Server info: %s v%s" server_info.name server_info.version);
+
+
(* Print environment info for debugging *)
+
log_debug "Environment variables:";
+
Unix.environment()
+
|> Array.iter (fun s ->
+
try
+
let i = String.index s '=' in
+
let name = String.sub s 0 i in
+
if String.length name > 0 then
+
log_debug (Printf.sprintf " %s" s)
+
with Not_found -> ()
+
);
+
+
let rec server_loop count =
+
log_debug (Printf.sprintf "Waiting for message #%d..." count);
+
match read_message () with
+
| Some json ->
+
log_debug (Printf.sprintf "Received message: %s" (Yojson.Safe.to_string json));
+
(match process_message json with
+
| Some response ->
+
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" response_str);
+
(* Make sure we emit properly formatted JSON on a single line with a newline at the end *)
+
Printf.printf "%s\n" response_str;
+
flush stdout;
+
(* Give the client a moment to process the response *)
+
Unix.sleepf 0.01;
+
server_loop (count + 1)
+
| None ->
+
log_debug "No response needed for this message";
+
server_loop (count + 1))
+
| None ->
+
log_debug "End of input stream, terminating server";
+
()
+
in
+
+
log_debug "Starting server loop...";
+
log_debug "Waiting for the initialize request...";
+
+
(* Set up signal handler to gracefully exit *)
+
Sys.(set_signal sigint (Signal_handle (fun _ ->
+
log_debug "Received interrupt signal, exiting...";
+
exit 0
+
)));
+
+
server_loop 1;
+
log_debug "Server terminated normally";
+
with
+
| End_of_file ->
+
log_error "Unexpected end of file";
+
| Sys_error msg ->
+
log_error (Printf.sprintf "System error: %s" msg);
+
| Unix.Unix_error(err, func, arg) ->
+
log_error (Printf.sprintf "Unix error in %s(%s): %s" func arg (Unix.error_message err));
+
| exc ->
+
log_error (Printf.sprintf "Unhandled exception: %s" (Printexc.to_string exc));
+
log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()))