Model Context Protocol in OCaml

mcp_server: turn into an actual server

Partly vibecoded, done using cohttp-eio

mseri.me 37c41d48 c17afd8f

Changed files
+44 -30
lib
+2
dune-project
···
(depends
(ocaml (>= "5.2.0"))
jsonrpc
+
http
+
cohttp-eio
eio_main
eio))
+1 -1
lib/dune
···
(library
(name mcp_server)
(public_name mcp.server)
-
(libraries mcp_sdk jsonrpc eio_main eio)
+
(libraries mcp_sdk jsonrpc eio_main eio http cohttp-eio)
(modules mcp_server)
)
+41 -29
lib/mcp_server.ml
···
Eio.Flow.copy_string "\n" stdout
(* Run the MCP server with the given server configuration *)
-
let run_server env server =
-
let stdin = Eio.Stdenv.stdin env in
-
let stdout = Eio.Stdenv.stdout env in
+
let callback mcp_server _conn (request : Http.Request.t) body =
+
match request.meth with
+
| `POST -> (
+
Log.debug "Received POST request";
+
let request_body_str =
+
Eio.Buf_read.(parse_exn take_all) body ~max_size:max_int
+
in
+
match process_input_line mcp_server request_body_str with
+
| 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;
+
let headers =
+
Http.Header.of_list [ ("Content-Type", "application/json") ]
+
in
+
Cohttp_eio.Server.respond ~status:`OK ~headers
+
~body:(Cohttp_eio.Body.of_string response_str)
+
()
+
| None ->
+
Log.debug "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);
+
Cohttp_eio.Server.respond ~status:`Method_not_allowed
+
~body:(Cohttp_eio.Body.of_string "Only POST is supported")
+
()
+
+
let log_warning ex = Logs.warn (fun f -> f "%a" Eio.Exn.pp ex)
+
+
let run_server ?(port = 8080) ?(on_error = log_warning) env server =
+
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);
-
(* Enable exception backtraces *)
-
Printexc.record_backtrace true;
-
-
let buf = Eio.Buf_read.of_flow stdin ~initial_size:100 ~max_size:1_000_000 in
-
-
(* Main processing loop *)
-
try
-
while true do
-
Log.debug "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"
-
done
-
with
-
| End_of_file ->
-
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);
-
()
+
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;
+
+
Cohttp_eio.Server.run server_socket server_spec ~on_error