Model Context Protocol in OCaml

logging: remove handcrafted logger and replace it with logs

The old one did not allow customization, and using logs will also make this behave like its dependencies.

Signed-off-by: Marcello Seri <marcello.seri@gmail.com>

+1
.gitignore
···
jsonrpc.mli
_build
CLAUDE.md
+
*.install
+3 -3
bin/capitalize_sdk.ml
···
TextContent.yojson_of_t
TextContent.{ text = capitalized_text; annotations = None }
with Failure msg ->
-
Log.errorf "Error in capitalize tool: %s" msg;
+
Logs.err (fun m -> m "Error in capitalize tool: %s" msg);
TextContent.yojson_of_t
TextContent.
{ text = Printf.sprintf "Error: %s" msg; annotations = None })
···
])
let () =
-
(* Run the server with the default scheduler *)
-
Eio_main.run @@ fun env -> Mcp_server.run_server env server
+
Logs.set_reporter (Logs.format_reporter ());
+
Eio_main.run @@ fun env -> Mcp_server.run_stdio_server env server
+4 -3
bin/dune
···
(executable
(name capitalize_sdk)
(modules capitalize_sdk)
-
(libraries mcp mcp_server yojson eio_main eio))
+
(libraries logs mcp mcp_server yojson eio_main eio))
(executable
(name multimodal_sdk)
(modules multimodal_sdk)
-
(libraries mcp mcp_sdk mcp_server yojson eio_main eio))
+
(libraries logs mcp mcp_sdk mcp_server yojson eio_main eio))
(executable
(name ocaml_eval_sdk)
···
(flags
(:standard -w -32 -w -33))
(libraries
+
logs
mcp
mcp_sdk
mcp_server
···
(executable
(name markdown_book_sdk)
(modules markdown_book_sdk)
-
(libraries mcp mcp_sdk mcp_server yojson eio_main eio))
+
(libraries logs mcp mcp_sdk mcp_server yojson eio_main eio))
+20 -20
bin/markdown_book_sdk.ml
···
```ocaml
(* If expression *)
-
let abs x =
+
let abs x =
if x < 0 then -x else x
(* Match expression (pattern matching) *)
···
let pi = 3.14159
let square x = x *. x
let cube x = x *. x *. x
-
+
(* This is hidden because it's not in the signature *)
let private_helper x = x +. 1.0
end
···
(* Functor that creates a set implementation given an element type with comparison *)
module MakeSet (Element : sig type t val compare : t -> t -> int end) : COLLECTION with type 'a t = Element.t list = struct
type 'a t = Element.t list
-
+
let empty = []
-
+
let rec add x lst =
match lst with
| [] -> [x]
···
if c < 0 then x :: lst
else if c = 0 then lst (* Element already exists *)
else y :: add x ys
-
+
let rec mem x lst =
match lst with
| [] -> false
···
| `Circle r -> Float.pi *. r *. r
| `Rectangle (w, h) -> w *. h
| `Triangle (b, h) -> 0.5 *. b *. h
-
| `Regular_polygon(n, s) when n >= 3 ->
+
| `Regular_polygon(n, s) when n >= 3 ->
let apothem = s /. (2.0 *. tan (Float.pi /. float_of_int n)) in
n *. s *. apothem /. 2.0
| _ -> failwith "Invalid shape"
···
object (self)
val mutable x = x_init
val mutable y = y_init
-
+
method get_x = x
method get_y = y
method move dx dy = x <- x + dx; y <- y + dy
-
method distance_from_origin =
+
method distance_from_origin =
sqrt (float_of_int (x * x + y * y))
-
+
(* Private method *)
-
method private to_string =
+
method private to_string =
Printf.sprintf "(%d, %d)" x y
-
+
(* Calling another method *)
method print = print_endline self#to_string
end
···
(* Handler for the Ask effect *)
let prompt_user () =
Effect.Deep.try_with
-
(fun () ->
+
(fun () ->
let name = Effect.perform (Ask "What is your name?") in
Printf.printf "Hello, %s!\n" name)
{ Effect.Deep.effc = fun (type a) (effect : a Effect.t) ->
···
} [@@deriving sexp]
(* With ppx_let for monadic operations *)
-
let computation =
+
let computation =
[%m.let
let* x = get_value_from_db "key1" in
let* y = get_value_from_db "key2" in
···
(* Phantom types for added type safety *)
module SafeString : sig
type 'a t
-
+
(* Constructors for different string types *)
val of_raw : string -> [`Raw] t
val sanitize : [`Raw] t -> [`Sanitized] t
val validate : [`Sanitized] t -> [`Validated] t option
-
+
(* Operations that require specific string types *)
val to_html : [`Sanitized] t -> string
val to_sql : [`Validated] t -> string
-
+
(* Common operations for all string types *)
val length : _ t -> int
val concat : _ t -> _ t -> [`Raw] t
end = struct
type 'a t = string
-
+
let of_raw s = s
let sanitize s = String.map (function '<' | '>' -> '_' | c -> c) s
let validate s = if String.length s > 0 then Some s else None
-
+
let to_html s = s
let to_sql s = "'" ^ String.map (function '\'' -> '\'' | c -> c) s ^ "'"
-
+
let length = String.length
let concat s1 s2 = s1 ^ s2
end
···
content)
(* Run the server with the default scheduler *)
-
let () = Eio_main.run @@ fun env -> Mcp_server.run_server env server
+
let () = Eio_main.run @@ fun env -> Mcp_server.run_stdio_server env server
+5 -4
bin/multimodal_sdk.ml
···
]
~is_error:false
with Failure msg ->
-
Log.errorf "Error in multimodal tool: %s" msg;
+
Logs.err (fun m -> m "Error in multimodal tool: %s" msg);
Tool.create_tool_result
[ Mcp.make_text_content (Printf.sprintf "Error: %s" msg) ]
~is_error:true)
···
[ Mcp.make_image_content image_data "image/gif" ]
~is_error:false
with Failure msg ->
-
Log.errorf "Error in generate_image tool: %s" msg;
+
Logs.err (fun m -> m "Error in generate_image tool: %s" msg);
Tool.create_tool_result
[ Mcp.make_text_content (Printf.sprintf "Error: %s" msg) ]
~is_error:true)
···
[ Mcp.make_audio_content audio_data "audio/wav" ]
~is_error:false
with Failure msg ->
-
Log.errorf "Error in generate_audio tool: %s" msg;
+
Logs.err (fun m -> m "Error in generate_audio tool: %s" msg);
Tool.create_tool_result
[ Mcp.make_text_content (Printf.sprintf "Error: %s" msg) ]
~is_error:true)
···
(* Run the server with the default scheduler *)
let () =
+
Logs.set_reporter (Logs.format_reporter ());
Random.self_init ();
(* Initialize random generator *)
-
Eio_main.run @@ fun env -> Mcp_server.run_server env server
+
Eio_main.run @@ fun env -> Mcp_server.run_stdio_server env server
+4 -2
bin/ocaml_eval_sdk.ml
···
[ Mcp.make_text_content output ]
~is_error:(not success)
with Failure msg ->
-
Log.errorf "Error in OCaml eval tool: %s" msg;
+
Logs.err (fun m -> m "Error in OCaml eval tool: %s" msg);
Tool.create_tool_result
[ Mcp.make_text_content (Printf.sprintf "Error: %s" msg) ]
~is_error:true)
(* Run the server with the default scheduler *)
-
let () = Eio_main.run @@ fun env -> Mcp_server.run_server env server
+
let () =
+
Logs.set_reporter (Logs.format_reporter ());
+
Eio_main.run @@ fun env -> Mcp_server.run_stdio_server env server
+2 -3
dune-project
···
(depends
(ocaml (>= "5.2.0"))
jsonrpc
-
(yojson (< "3.0.0"))
http
cohttp-eio
eio_main
-
eio))
-
+
eio
+
logs))
+1 -1
lib/dune
···
(library
(name mcp_sdk)
(public_name mcp.sdk)
-
(libraries mcp mcp_rpc jsonrpc unix yojson)
+
(libraries mcp mcp_rpc jsonrpc unix yojson logs logs.fmt)
(modules mcp_sdk)
(flags
(:standard -w -67 -w -27 -w -32)))
+8 -35
lib/mcp_sdk.ml
···
(* SDK version *)
let version = "0.1.0"
-
-
(* Logging utilities *)
-
module Log = struct
-
type level = Debug | Info | Warning | Error
-
-
let string_of_level = function
-
| Debug -> "DEBUG"
-
| Info -> "INFO"
-
| 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
+
let src = Logs.Src.create "mcp.sdk" ~doc:"mcp.sdk logging"
-
(* 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
+
module Log = (val Logs.src_log src : Logs.LOG)
(* Context for tools and resources *)
module Context = struct
···
(* Create a tool error result with structured content *)
let create_error_result error =
-
Log.errorf "Error result: %s" error;
+
Logs.err (fun m -> m "Error result: %s" error);
create_tool_result [ Mcp.make_text_content error ] ~is_error:true
(* Handle tool execution errors *)
···
let create ~uri ~name ?description ?mime_type ~handler () =
(* Validate that the URI doesn't contain template variables *)
if String.contains uri '{' || String.contains uri '}' then
-
Log.warningf
-
"Resource '%s' contains template variables. Consider using \
-
add_resource_template instead."
-
uri;
+
Logs.warn (fun m ->
+
m
+
"Resource '%s' contains template variables. Consider using \
+
add_resource_template instead."
+
uri);
{ uri; name; description; mime_type; handler }
let to_json resource =
-23
lib/mcp_sdk.mli
···
val version : string
(** SDK version *)
-
(** Logging utilities *)
-
module Log : sig
-
type level = Debug | Info | Warning | Error
-
-
val string_of_level : level -> string
-
-
val logf : level -> ('a, out_channel, unit) format -> 'a
-
(** Format-string based logging functions *)
-
-
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
-
-
val log : level -> string -> unit
-
(** Simple string logging functions (for backward compatibility) *)
-
-
val debug : string -> unit
-
val info : string -> unit
-
val warning : string -> unit
-
val error : string -> unit
-
end
-
(** Context for tools and resources *)
module Context : sig
type t
+93 -70
lib/mcp_server.ml
···
open Jsonrpc
open Mcp_sdk
+
let src = Logs.Src.create "mcp.sdk" ~doc:"mcp.sdk logging"
+
+
module Log = (val Logs.src_log src : Logs.LOG)
+
(* Create a proper JSONRPC error with code and data *)
let create_jsonrpc_error id code message ?data () =
let error_code = ErrorCode.to_int code in
···
(* Process initialize request *)
let handle_initialize server req =
-
Log.debug "Processing initialize request";
+
Log.debug (fun m -> m "Processing initialize request");
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;
+
Logs.debug (fun m ->
+
m "Client info: %s v%s" req_data.client_info.name
+
req_data.client_info.version);
+
Log.debug (fun m ->
+
m "Client protocol version: %s" req_data.protocol_version);
(* Create initialize response *)
let result =
···
in
Initialize.Result.yojson_of_t result
| None ->
-
Log.error "Missing params for initialize request";
+
Log.err (fun m -> m "Missing params for initialize request");
`Assoc [ ("error", `String "Missing params for initialize request") ]
in
Some (create_response ~id:req.id ~result)
(* Process tools/list request *)
let handle_tools_list server (req : JSONRPCMessage.request) =
-
Log.debug "Processing tools/list request";
+
Log.debug (fun m -> m "Processing tools/list request");
let tools_list = Tool.to_rpc_tools_list (tools server) in
let response =
Mcp_rpc.ToolsList.create_response ~id:req.id ~tools:tools_list ()
···
(* Process prompts/list request *)
let handle_prompts_list server (req : JSONRPCMessage.request) =
-
Log.debug "Processing prompts/list request";
+
Log.debug (fun m -> m "Processing prompts/list request");
let prompts_list = Prompt.to_rpc_prompts_list (prompts server) in
let response =
Mcp_rpc.PromptsList.create_response ~id:req.id ~prompts:prompts_list ()
···
(* Process resources/list request *)
let handle_resources_list server (req : JSONRPCMessage.request) =
-
Log.debug "Processing resources/list request";
+
Log.debug (fun m -> m "Processing resources/list request");
let resources_list = Resource.to_rpc_resources_list (resources server) in
let response =
Mcp_rpc.ResourcesList.create_response ~id:req.id ~resources:resources_list
···
(* Process resources/templates/list request *)
let handle_resource_templates_list server (req : JSONRPCMessage.request) =
-
Log.debug "Processing resources/templates/list request";
+
Log.debug (fun m -> m "Processing resources/templates/list request");
let templates_list =
ResourceTemplate.to_rpc_resource_templates_list (resource_templates server)
in
···
(* Process resources/read request *)
let handle_resources_read server (req : JSONRPCMessage.request) =
-
Log.debug "Processing resources/read request";
+
Log.debug (fun m -> m "Processing resources/read request");
match req.JSONRPCMessage.params with
| None ->
-
Log.error "Missing params for resources/read request";
+
Log.err (fun m -> m "Missing params for resources/read request");
Some
(create_jsonrpc_error req.id ErrorCode.InvalidParams
"Missing params for resources/read request" ())
| Some params -> (
let req_data = Mcp_rpc.ResourcesRead.Request.t_of_yojson params in
let uri = req_data.uri in
-
Log.debugf "Resource URI: %s" uri;
+
Log.debug (fun m -> m "Resource URI: %s" uri);
(* Find matching resource or template *)
match Resource_matcher.find_match server uri with
···
()
in
-
Log.debugf "Handling direct resource: %s" resource.name;
+
Log.debug (fun m -> m "Handling direct resource: %s" resource.name);
(* Call the resource handler *)
match resource.handler ctx params with
···
in
Some response
| Error err ->
-
Log.errorf "Error reading resource: %s" err;
+
Log.err (fun m -> m "Error reading resource: %s" err);
Some
(create_jsonrpc_error req.id ErrorCode.InternalError
("Error reading resource: " ^ err)
···
()
in
-
Log.debugf "Handling resource template: %s with params: [%s]"
-
template.name
-
(String.concat ", " params);
+
Log.debug (fun m ->
+
m "Handling resource template: %s with params: [%s]" template.name
+
(String.concat ", " params));
(* Call the template handler *)
match template.handler ctx params with
···
in
Some response
| Error err ->
-
Log.errorf "Error reading resource template: %s" err;
+
Log.err (fun m -> m "Error reading resource template: %s" err);
Some
(create_jsonrpc_error req.id ErrorCode.InternalError
("Error reading resource template: " ^ err)
()))
| Resource_matcher.NoMatch ->
-
Log.errorf "Resource not found: %s" uri;
+
Log.err (fun m -> m "Resource not found: %s" uri);
Some
(create_jsonrpc_error req.id ErrorCode.InvalidParams
("Resource not found: " ^ uri)
···
let extract_tool_name params =
match List.assoc_opt "name" params with
| Some (`String name) ->
-
Log.debugf "Tool name: %s" name;
+
Log.debug (fun m -> m "Tool name: %s" name);
Some name
| _ ->
-
Log.error "Missing or invalid 'name' parameter in tool call";
+
Log.err (fun m -> m "Missing or invalid 'name' parameter in tool call");
None
(* Extract the tool arguments from params *)
let extract_tool_arguments params =
match List.assoc_opt "arguments" params with
| Some args ->
-
Log.debugf "Tool arguments: %s" (Yojson.Safe.to_string args);
+
Log.debug (fun m -> m "Tool arguments: %s" (Yojson.Safe.to_string args));
args
| _ ->
-
Log.debug "No arguments provided for tool call, using empty object";
+
Log.debug (fun m ->
+
m "No arguments provided for tool call, using empty object");
`Assoc [] (* Empty arguments is valid *)
(* Execute a tool *)
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;
+
Log.debug (fun m -> m "Found tool: %s" name);
(* Call the tool handler *)
match tool.handler ctx args with
| Ok result ->
-
Log.debug "Tool execution succeeded";
+
Log.debug (fun m -> m "Tool execution succeeded");
result
| Error err -> Tool.handle_execution_error err
with
···
(* Process tools/call request *)
let handle_tools_call server req =
-
Log.debug "Processing tools/call request";
+
Log.debug (fun m -> m "Processing tools/call request");
match req.JSONRPCMessage.params with
| Some (`Assoc params) -> (
match extract_tool_name params with
···
(create_jsonrpc_error req.id InvalidParams
"Missing tool name parameter" ()))
| _ ->
-
Log.error "Invalid params format for tools/call";
+
Log.err (fun m -> m "Invalid params format for tools/call");
Some
(create_jsonrpc_error req.id InvalidParams
"Invalid params format for tools/call" ())
(* Process ping request *)
let handle_ping (req : JSONRPCMessage.request) =
-
Log.debug "Processing ping request";
+
Log.debug (fun m -> m "Processing ping request");
Some (create_response ~id:req.JSONRPCMessage.id ~result:(`Assoc []))
(* 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");
+
Log.debug (fun m ->
+
m
+
"Client initialization complete - Server is now ready to receive \
+
requests\n\
+
\ 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);
+
Log.debug (fun m ->
+
m "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);
+
Log.debug (fun m ->
+
m "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.ResourceTemplatesList ->
handle_resource_templates_list server req
| _ ->
-
Log.errorf "Unknown method received: %s" (Method.to_string req.meth);
+
Log.err (fun m ->
+
m "Unknown method received: %s" (Method.to_string req.meth));
Some
(create_jsonrpc_error req.id ErrorCode.MethodNotFound
("Method not found: " ^ Method.to_string req.meth)
()))
| JSONRPCMessage.Notification notif -> (
-
Log.debugf "Received notification with method: %s"
-
(Method.to_string notif.meth);
+
Log.debug (fun m ->
+
m "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);
+
Log.debug (fun m ->
+
m "Ignoring notification: %s" (Method.to_string notif.meth));
None)
| JSONRPCMessage.Response _ ->
-
Log.error "Unexpected response message received";
+
Log.err (fun m -> m "Unexpected response message received");
None
| JSONRPCMessage.Error _ ->
-
Log.error "Unexpected error message received";
+
Log.err (fun m -> m "Unexpected error message received");
None
with
| Json.Of_json (msg, _) ->
-
Log.errorf "JSON error: %s" msg;
+
Log.err (fun m -> m "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;
+
Log.err (fun m -> m "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);
+
Log.err (fun m ->
+
m
+
"Exception during message processing: %s\n\
+
Backtrace: %s\n\
+
Message was: %s"
+
(Printexc.to_string exc)
+
(Printexc.get_backtrace ())
+
(Yojson.Safe.to_string message));
None
(* Extract a request ID from a potentially malformed message *)
···
(* Handle processing for an input line *)
let process_input_line server line =
if line = "" then (
-
Log.debug "Empty line received, ignoring";
+
Log.debug (fun m -> m "Empty line received, ignoring");
None)
else (
-
Log.debugf "Raw input: %s" line;
+
Log.debug (fun m -> m "Raw input: %s" line);
try
let json = Yojson.Safe.from_string line in
-
Log.debug "Successfully parsed JSON";
+
Log.debug (fun m -> m "Successfully parsed JSON");
(* Process the message *)
process_message server json
with Yojson.Json_error msg ->
-
Log.errorf "Error parsing JSON: %s" msg;
-
Log.errorf "Input was: %s" line;
+
Log.err (fun m -> m "Error parsing JSON: %s" msg);
+
Log.err (fun m -> m "Input was: %s" line);
None)
(* Send a response to the client *)
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;
+
Log.debug (fun m -> m "Sending response: %s" response_str);
(* Write the response followed by a newline *)
Eio.Flow.copy_string response_str stdout;
···
let callback mcp_server _conn (request : Http.Request.t) body =
match request.meth with
| `POST -> (
-
Log.debug "Received POST request";
+
Log.debug (fun m -> m "Received POST request");
let request_body_str =
Eio.Buf_read.(parse_exn take_all) body ~max_size:max_int
in
···
| Some mcp_response ->
let response_json = JSONRPCMessage.yojson_of_t mcp_response in
let response_str = Yojson.Safe.to_string response_json in
-
Log.debugf "Sending MCP response: %s" response_str;
+
Log.debug (fun m -> m "Sending MCP response: %s" response_str);
let headers =
Http.Header.of_list [ ("Content-Type", "application/json") ]
in
···
~body:(Cohttp_eio.Body.of_string response_str)
()
| None ->
-
Log.debug "No MCP response needed";
+
Log.debug (fun m -> m "No MCP response needed");
Cohttp_eio.Server.respond ~status:`No_content
~body:(Cohttp_eio.Body.of_string "")
())
| _ ->
-
Log.infof "Unsupported method: %s" (Http.Method.to_string request.meth);
+
Log.info (fun m ->
+
m "Unsupported method: %s" (Http.Method.to_string request.meth));
Cohttp_eio.Server.respond ~status:`Method_not_allowed
~body:(Cohttp_eio.Body.of_string "Only POST is supported")
()
···
let net = Eio.Stdenv.net env in
let addr = `Tcp (Eio.Net.Ipaddr.V4.loopback, port) in
-
Log.debugf "Starting MCP server: %s v%s" (name server) (version server);
-
Log.debugf "Protocol version: %s" (protocol_version server);
+
Log.info (fun m ->
+
m "Starting http MCP server: %s v%s\nProtocol version: %s" (name server)
+
(version server) (protocol_version server));
Eio.Switch.run @@ fun sw ->
let server_spec = Cohttp_eio.Server.make ~callback:(callback server) () in
···
let server_socket =
Eio.Net.listen net ~sw ~backlog:128 ~reuse_addr:true addr
in
-
Log.infof "MCP HTTP Server listening on http://localhost:%d" port;
+
Log.info (fun m -> m "MCP HTTP Server listening on http://localhost:%d" port);
Cohttp_eio.Server.run server_socket server_spec ~on_error
(** run the server using the stdio transport *)
-
let run_sdtio_server env server =
+
let run_stdio_server env server =
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);
+
Log.info (fun m ->
+
m "Starting stdio MCP server: %s v%s\nProtocol version: %s" (name server)
+
(version server) (protocol_version server));
(* Enable exception backtraces *)
Printexc.record_backtrace true;
···
(* Main processing loop *)
try
while true do
-
Log.debug "Waiting for message...";
+
Log.info (fun m -> m "Waiting for message...");
let line = Eio.Buf_read.line buf in
(* Process the input and send response if needed *)
match process_input_line server line with
| Some response -> send_response stdout response
-
| None -> Log.debug "No response needed for this message"
+
| None -> Log.info (fun m -> m "No response needed for this message")
done
with
| End_of_file ->
-
Log.debug "End of file received on stdin";
+
Log.debug (fun m -> m "End of file received on stdin");
()
| Eio.Exn.Io _ as exn ->
-
Log.errorf "I/O error while reading: %s" (Printexc.to_string exn);
+
(* Only a warning since on Windows, once the client closes the connection, we normally fail with `I/O error while reading: Eio.Io Net Connection_reset Unix_error (Broken pipe, "stub_cstruct_read", "")` *)
+
Log.warn (fun m ->
+
m "I/O error while reading: %s" (Printexc.to_string exn));
()
| exn ->
-
Log.errorf "Exception while reading: %s" (Printexc.to_string exn);
+
Log.err (fun m ->
+
m "Exception while reading: %s" (Printexc.to_string exn));
()
+1 -1
mcp.opam
···
"dune" {>= "3.17"}
"ocaml" {>= "5.2.0"}
"jsonrpc"
-
"yojson" {< "3.0.0"}
"http"
"cohttp-eio"
"eio_main"
"eio"
+
"logs"
"odoc" {with-doc}
]
build: [