Model Context Protocol in OCaml

Enhance Log module with format string support

- Add logf, debugf, infof, warningf, and errorf functions that take format strings
- Keep original log functions for backward compatibility
- Update logging calls across the codebase to use format string versions
- Simplify code by removing unnecessary Printf.sprintf calls

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

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

+1 -1
bin/capitalize_sdk.ml
···
}
with
| Failure msg ->
-
Log.error (Printf.sprintf "Error in capitalize tool: %s" msg);
TextContent.yojson_of_t TextContent.{
text = Printf.sprintf "Error: %s" msg;
annotations = None
···
}
with
| Failure msg ->
+
Log.errorf "Error in capitalize tool: %s" msg;
TextContent.yojson_of_t TextContent.{
text = Printf.sprintf "Error: %s" msg;
annotations = None
+3 -3
bin/multimodal_sdk.ml
···
] ~is_error:false
with
| Failure msg ->
-
Log.error (Printf.sprintf "Error in multimodal tool: %s" msg);
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
~is_error:false
with
| Failure msg ->
-
Log.error (Printf.sprintf "Error in generate_image tool: %s" msg);
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
~is_error:false
with
| Failure msg ->
-
Log.error (Printf.sprintf "Error in generate_audio tool: %s" msg);
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
] ~is_error:false
with
| Failure msg ->
+
Log.errorf "Error in multimodal tool: %s" msg;
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
~is_error:false
with
| Failure msg ->
+
Log.errorf "Error in generate_image tool: %s" msg;
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
~is_error:false
with
| Failure msg ->
+
Log.errorf "Error in generate_audio tool: %s" msg;
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
+1 -1
bin/ocaml_eval_sdk.ml
···
with
| Failure msg ->
-
Log.error (Printf.sprintf "Error in OCaml eval tool: %s" msg);
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
···
with
| Failure msg ->
+
Log.errorf "Error in OCaml eval tool: %s" msg;
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
+17 -10
lib/mcp_sdk.ml
···
| Warning -> "WARNING"
| Error -> "ERROR"
-
let log level msg =
-
Printf.eprintf "[%s] %s\n" (string_of_level level) msg;
-
flush stderr
-
let debug = log Debug
-
let info = log Info
-
let warning = log Warning
-
let error = log Error
end
(* Context for tools and resources *)
···
(* Handle tool execution errors *)
let handle_execution_error err =
-
Log.error (Printf.sprintf "Tool execution failed: %s" err);
create_error_result (Printf.sprintf "Error executing tool: %s" err)
(* Handle unknown tool error *)
let handle_unknown_tool_error name =
-
Log.error (Printf.sprintf "Unknown tool: %s" name);
create_error_result (Printf.sprintf "Unknown tool: %s" name)
(* Handle general tool execution exception *)
let handle_execution_exception exn =
-
Log.error (Printf.sprintf "Exception executing tool: %s" (Printexc.to_string exn));
create_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn))
end
···
| Warning -> "WARNING"
| Error -> "ERROR"
+
let logf level fmt =
+
Printf.fprintf stderr "[%s] " (string_of_level level);
+
Printf.kfprintf (fun oc -> Printf.fprintf oc "\n"; flush oc) stderr fmt
+
let debugf fmt = logf Debug fmt
+
let infof fmt = logf Info fmt
+
let warningf fmt = logf Warning fmt
+
let errorf fmt = logf Error fmt
+
+
(* Backward compatibility functions that take a simple string *)
+
let log level msg = logf level "%s" msg
+
let debug msg = debugf "%s" msg
+
let info msg = infof "%s" msg
+
let warning msg = warningf "%s" msg
+
let error msg = errorf "%s" msg
end
(* Context for tools and resources *)
···
(* Handle tool execution errors *)
let handle_execution_error err =
+
Log.errorf "Tool execution failed: %s" err;
create_error_result (Printf.sprintf "Error executing tool: %s" err)
(* Handle unknown tool error *)
let handle_unknown_tool_error name =
+
Log.errorf "Unknown tool: %s" name;
create_error_result (Printf.sprintf "Unknown tool: %s" name)
(* Handle general tool execution exception *)
let handle_execution_exception exn =
+
Log.errorf "Exception executing tool: %s" (Printexc.to_string exn);
create_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn))
end
+8
lib/mcp_sdk.mli
···
val string_of_level : level -> string
val log : level -> string -> unit
val debug : string -> unit
val info : string -> unit
···
val string_of_level : level -> string
+
(** Format-string based logging functions *)
+
val logf : level -> ('a, out_channel, unit) format -> 'a
+
val debugf : ('a, out_channel, unit) format -> 'a
+
val infof : ('a, out_channel, unit) format -> 'a
+
val warningf : ('a, out_channel, unit) format -> 'a
+
val errorf : ('a, out_channel, unit) format -> 'a
+
+
(** Simple string logging functions (for backward compatibility) *)
val log : level -> string -> unit
val debug : string -> unit
val info : string -> unit
+26 -26
lib/mcp_server.ml
···
let result = match req.JSONRPCMessage.params with
| Some params ->
let req_data = Initialize.Request.t_of_yojson params in
-
Log.debug (Printf.sprintf "Client info: %s v%s"
-
req_data.client_info.name req_data.client_info.version);
-
Log.debug (Printf.sprintf "Client protocol version: %s" req_data.protocol_version);
(* Create initialize response *)
let result = Initialize.Result.create
···
let extract_tool_name params =
match List.assoc_opt "name" params with
| Some (`String name) ->
-
Log.debug (Printf.sprintf "Tool name: %s" name);
Some name
| _ ->
Log.error "Missing or invalid 'name' parameter in tool call";
···
let extract_tool_arguments params =
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";
···
let execute_tool server ctx name args =
try
let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
-
Log.debug (Printf.sprintf "Found tool: %s" name);
(* Call the tool handler *)
match tool.handler ctx args with
···
(* Handle notifications/initialized *)
let handle_initialized (notif:JSONRPCMessage.notification) =
Log.debug "Client initialization complete - Server is now ready to receive requests";
-
Log.debug (Printf.sprintf "Notification params: %s"
(match notif.JSONRPCMessage.params with
| Some p -> Yojson.Safe.to_string p
-
| None -> "null"));
None
(* Process a single message using the MCP SDK *)
let process_message server message =
try
-
Log.debug (Printf.sprintf "Processing message: %s" (Yojson.Safe.to_string message));
match JSONRPCMessage.t_of_yojson message with
| JSONRPCMessage.Request req ->
-
Log.debug (Printf.sprintf "Received request with method: %s" (Method.to_string req.meth));
(match req.meth with
| Method.Initialize -> handle_initialize server req
| Method.ToolsList -> handle_tools_list server req
···
| Method.PromptsList -> handle_prompts_list server req
| Method.ResourcesList -> handle_resources_list server req
| _ ->
-
Log.error (Printf.sprintf "Unknown method received: %s" (Method.to_string req.meth));
Some (create_jsonrpc_error req.id MethodNotFound ("Method not found: " ^ (Method.to_string req.meth)) ()))
| JSONRPCMessage.Notification notif ->
-
Log.debug (Printf.sprintf "Received notification with method: %s" (Method.to_string notif.meth));
(match notif.meth with
| Method.Initialized -> handle_initialized notif
| _ ->
-
Log.debug (Printf.sprintf "Ignoring notification: %s" (Method.to_string notif.meth));
None)
| JSONRPCMessage.Response _ ->
Log.error "Unexpected response message received";
···
None
with
| Json.Of_json (msg, _) ->
-
Log.error (Printf.sprintf "JSON error: %s" msg);
(* Can't respond with error because we don't have a request ID *)
None
| Yojson.Json_error msg ->
-
Log.error (Printf.sprintf "JSON parse error: %s" msg);
(* Can't respond with error because we don't have a request ID *)
None
| 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
(* Extract a request ID from a potentially malformed message *)
···
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";
···
process_message server json
with
| Yojson.Json_error msg -> begin
-
Log.error (Printf.sprintf "Error parsing JSON: %s" msg);
-
Log.error (Printf.sprintf "Input was: %s" line);
None
end
)
···
let send_response stdout 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);
(* Write the response followed by a newline *)
Eio.Flow.copy_string response_str stdout;
···
let stdin = Eio.Stdenv.stdin env in
let stdout = Eio.Stdenv.stdout env in
-
Log.debug (Printf.sprintf "Starting MCP server: %s v%s" (name server) (version server));
-
Log.debug (Printf.sprintf "Protocol version: %s" (protocol_version server));
(* Enable exception backtraces *)
Printexc.record_backtrace true;
···
Log.debug "End of file received on stdin";
()
| Eio.Exn.Io _ as exn ->
-
Log.error (Printf.sprintf "I/O error while reading: %s" (Printexc.to_string exn));
()
| exn ->
-
Log.error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exn));
()
···
let result = match req.JSONRPCMessage.params with
| Some params ->
let req_data = Initialize.Request.t_of_yojson params in
+
Log.debugf "Client info: %s v%s"
+
req_data.client_info.name req_data.client_info.version;
+
Log.debugf "Client protocol version: %s" req_data.protocol_version;
(* Create initialize response *)
let result = Initialize.Result.create
···
let extract_tool_name params =
match List.assoc_opt "name" params with
| Some (`String name) ->
+
Log.debugf "Tool name: %s" name;
Some name
| _ ->
Log.error "Missing or invalid 'name' parameter in tool call";
···
let extract_tool_arguments params =
match List.assoc_opt "arguments" params with
| Some (args) ->
+
Log.debugf "Tool arguments: %s" (Yojson.Safe.to_string args);
args
| _ ->
Log.debug "No arguments provided for tool call, using empty object";
···
let execute_tool server ctx name args =
try
let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
+
Log.debugf "Found tool: %s" name;
(* Call the tool handler *)
match tool.handler ctx args with
···
(* Handle notifications/initialized *)
let handle_initialized (notif:JSONRPCMessage.notification) =
Log.debug "Client initialization complete - Server is now ready to receive requests";
+
Log.debugf "Notification params: %s"
(match notif.JSONRPCMessage.params with
| Some p -> Yojson.Safe.to_string p
+
| None -> "null");
None
(* Process a single message using the MCP SDK *)
let process_message server message =
try
+
Log.debugf "Processing message: %s" (Yojson.Safe.to_string message);
match JSONRPCMessage.t_of_yojson message with
| JSONRPCMessage.Request req ->
+
Log.debugf "Received request with method: %s" (Method.to_string req.meth);
(match req.meth with
| Method.Initialize -> handle_initialize server req
| Method.ToolsList -> handle_tools_list server req
···
| Method.PromptsList -> handle_prompts_list server req
| Method.ResourcesList -> handle_resources_list server req
| _ ->
+
Log.errorf "Unknown method received: %s" (Method.to_string req.meth);
Some (create_jsonrpc_error req.id MethodNotFound ("Method not found: " ^ (Method.to_string req.meth)) ()))
| JSONRPCMessage.Notification notif ->
+
Log.debugf "Received notification with method: %s" (Method.to_string notif.meth);
(match notif.meth with
| Method.Initialized -> handle_initialized notif
| _ ->
+
Log.debugf "Ignoring notification: %s" (Method.to_string notif.meth);
None)
| JSONRPCMessage.Response _ ->
Log.error "Unexpected response message received";
···
None
with
| Json.Of_json (msg, _) ->
+
Log.errorf "JSON error: %s" msg;
(* Can't respond with error because we don't have a request ID *)
None
| Yojson.Json_error msg ->
+
Log.errorf "JSON parse error: %s" msg;
(* Can't respond with error because we don't have a request ID *)
None
| exc ->
+
Log.errorf "Exception during message processing: %s" (Printexc.to_string exc);
+
Log.errorf "Backtrace: %s" (Printexc.get_backtrace());
+
Log.errorf "Message was: %s" (Yojson.Safe.to_string message);
None
(* Extract a request ID from a potentially malformed message *)
···
Log.debug "Empty line received, ignoring";
None
) else (
+
Log.debugf "Raw input: %s" line;
try
let json = Yojson.Safe.from_string line in
Log.debug "Successfully parsed JSON";
···
process_message server json
with
| Yojson.Json_error msg -> begin
+
Log.errorf "Error parsing JSON: %s" msg;
+
Log.errorf "Input was: %s" line;
None
end
)
···
let send_response stdout response =
let response_json = JSONRPCMessage.yojson_of_t response in
let response_str = Yojson.Safe.to_string response_json in
+
Log.debugf "Sending response: %s" response_str;
(* Write the response followed by a newline *)
Eio.Flow.copy_string response_str stdout;
···
let stdin = Eio.Stdenv.stdin env in
let stdout = Eio.Stdenv.stdout env in
+
Log.debugf "Starting MCP server: %s v%s" (name server) (version server);
+
Log.debugf "Protocol version: %s" (protocol_version server);
(* Enable exception backtraces *)
Printexc.record_backtrace true;
···
Log.debug "End of file received on stdin";
()
| Eio.Exn.Io _ as exn ->
+
Log.errorf "I/O error while reading: %s" (Printexc.to_string exn);
()
| exn ->
+
Log.errorf "Exception while reading: %s" (Printexc.to_string exn);
()