Model Context Protocol in OCaml
at tmp 13 kB view raw
1open Mcp 2open Jsonrpc 3 4(* Logging utilities *) 5let log_debug msg = 6 Printf.eprintf "[DEBUG] %s\n" msg; 7 flush stderr 8 9let log_error msg = 10 Printf.eprintf "[ERROR] %s\n" msg; 11 flush stderr 12 13(* Server state *) 14let protocol_version = "2024-11-05" 15let server_info = Implementation.{ name = "ocaml-mcp-capitalizer"; version = "0.1.0" } 16let server_capabilities = `Assoc [ 17 (* We support tools *) 18 ("tools", `Assoc [ 19 ("listChanged", `Bool true) 20 ]); 21 (* We don't support resources - make this explicit *) 22 ("resources", `Assoc [ 23 ("listChanged", `Bool false); 24 ("subscribe", `Bool false) 25 ]); 26 (* We don't support prompts - make this explicit *) 27 ("prompts", `Assoc [ 28 ("listChanged", `Bool false) 29 ]) 30] 31 32(* Tool implementation *) 33module CapitalizeTool = struct 34 let name = "capitalize" 35 let description = "Capitalizes the provided text" 36 let input_schema = `Assoc [ 37 ("type", `String "object"); 38 ("properties", `Assoc [ 39 ("text", `Assoc [ 40 ("type", `String "string"); 41 ("description", `String "The text to capitalize") 42 ]) 43 ]); 44 ("required", `List [`String "text"]) 45 ] 46 47 let call json = 48 match json with 49 | `Assoc fields -> 50 (match List.assoc_opt "text" fields with 51 | Some (`String text) -> 52 let capitalized_text = String.uppercase_ascii text in 53 let content = TextContent.{ 54 text = capitalized_text; 55 annotations = None 56 } in 57 `Assoc [ 58 ("content", `List [TextContent.yojson_of_t content]); 59 ("isError", `Bool false) 60 ] 61 | _ -> 62 let error_content = TextContent.{ 63 text = "Missing or invalid 'text' parameter"; 64 annotations = None 65 } in 66 `Assoc [ 67 ("content", `List [TextContent.yojson_of_t error_content]); 68 ("isError", `Bool true) 69 ]) 70 | _ -> 71 let error_content = TextContent.{ 72 text = "Invalid arguments format"; 73 annotations = None 74 } in 75 `Assoc [ 76 ("content", `List [TextContent.yojson_of_t error_content]); 77 ("isError", `Bool true) 78 ] 79end 80 81(* Handle tool listing *) 82let list_tools () = 83 let tool = `Assoc [ 84 ("name", `String CapitalizeTool.name); 85 ("description", `String CapitalizeTool.description); 86 ("inputSchema", CapitalizeTool.input_schema) 87 ] in 88 `Assoc [ 89 ("tools", `List [tool]) 90 ] 91 92(* Handle tool calls *) 93let call_tool name args = 94 if name = CapitalizeTool.name then 95 CapitalizeTool.call args 96 else 97 let error_content = TextContent.{ 98 text = Printf.sprintf "Unknown tool: %s" name; 99 annotations = None 100 } in 101 `Assoc [ 102 ("content", `List [TextContent.yojson_of_t error_content]); 103 ("isError", `Bool true) 104 ] 105 106(* Handle initialization *) 107let handle_initialize id json = 108 try 109 log_debug (Printf.sprintf "Processing initialize request with id: %s" 110 (match id with 111 | `Int i -> string_of_int i 112 | `String s -> s)); 113 114 log_debug (Printf.sprintf "Initialize params: %s" 115 (match json with 116 | Some j -> Yojson.Safe.to_string j 117 | None -> "null")); 118 119 let _ = match json with 120 | Some params -> 121 log_debug "Parsing initialize request params..."; 122 let req = Initialize.Request.t_of_yojson params in 123 log_debug (Printf.sprintf "Client info: %s v%s" req.client_info.name req.client_info.version); 124 log_debug (Printf.sprintf "Client protocol version: %s" req.protocol_version); 125 126 (* Check protocol version compatibility *) 127 if req.protocol_version <> protocol_version then 128 log_debug (Printf.sprintf "Protocol version mismatch: client=%s server=%s - will use server version" 129 req.protocol_version protocol_version); 130 131 req 132 | None -> 133 log_error "Missing params for initialize request"; 134 raise (Json.Of_json ("Missing params for initialize request", `Null)) 135 in 136 137 log_debug "Creating initialize response..."; 138 let result = Initialize.Result.create 139 ~capabilities:server_capabilities 140 ~server_info 141 ~protocol_version 142 ~instructions:"This server provides a tool to capitalize text." 143 () 144 in 145 146 log_debug "Serializing initialize response..."; 147 let response = create_response ~id ~result:(Initialize.Result.yojson_of_t result) in 148 log_debug "Initialize response created successfully"; 149 response 150 with 151 | Json.Of_json (msg, _) -> 152 log_error (Printf.sprintf "JSON error in initialize: %s" msg); 153 create_error ~id ~code:(-32602) ~message:("Invalid params: " ^ msg) () 154 | exc -> 155 log_error (Printf.sprintf "Exception in initialize: %s" (Printexc.to_string exc)); 156 log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace())); 157 create_error ~id ~code:(-32603) ~message:("Internal error: " ^ (Printexc.to_string exc)) () 158 159(* Handle tools/list *) 160let handle_list_tools id = 161 log_debug "Processing tools/list request"; 162 let result = list_tools () in 163 log_debug (Printf.sprintf "Tools list result: %s" (Yojson.Safe.to_string result)); 164 create_response ~id ~result 165 166(* Handle tools/call *) 167let handle_call_tool id json = 168 try 169 log_debug (Printf.sprintf "Processing tool call request with id: %s" 170 (match id with 171 | `Int i -> string_of_int i 172 | `String s -> s)); 173 174 log_debug (Printf.sprintf "Tool call params: %s" 175 (match json with 176 | Some j -> Yojson.Safe.to_string j 177 | None -> "null")); 178 179 match json with 180 | Some (`Assoc params) -> 181 let name = match List.assoc_opt "name" params with 182 | Some (`String name) -> 183 log_debug (Printf.sprintf "Tool name: %s" name); 184 name 185 | _ -> 186 log_error "Missing or invalid 'name' parameter in tool call"; 187 raise (Json.Of_json ("Missing or invalid 'name' parameter", `Assoc params)) 188 in 189 let args = match List.assoc_opt "arguments" params with 190 | Some (args) -> 191 log_debug (Printf.sprintf "Tool arguments: %s" (Yojson.Safe.to_string args)); 192 args 193 | _ -> 194 log_debug "No arguments provided for tool call, using empty object"; 195 `Assoc [] (* Empty arguments is valid *) 196 in 197 log_debug (Printf.sprintf "Calling tool: %s" name); 198 let result = call_tool name args in 199 log_debug (Printf.sprintf "Tool call result: %s" (Yojson.Safe.to_string result)); 200 create_response ~id ~result 201 | _ -> 202 log_error "Invalid params format for tools/call"; 203 create_error ~id ~code:(-32602) ~message:"Invalid params for tools/call" () 204 with 205 | Json.Of_json (msg, _) -> 206 log_error (Printf.sprintf "JSON error in tool call: %s" msg); 207 create_error ~id ~code:(-32602) ~message:("Invalid params: " ^ msg) () 208 | exc -> 209 log_error (Printf.sprintf "Exception in tool call: %s" (Printexc.to_string exc)); 210 log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace())); 211 create_error ~id ~code:(-32603) ~message:("Internal error: " ^ (Printexc.to_string exc)) () 212 213(* Handle ping *) 214let handle_ping id = 215 create_response ~id ~result:(`Assoc []) 216 217(* Process a single message *) 218let process_message message = 219 try 220 log_debug "Parsing message as JSONRPC message..."; 221 match JSONRPCMessage.t_of_yojson message with 222 | JSONRPCMessage.Request req -> 223 log_debug (Printf.sprintf "Received request with method: %s" req.method_); 224 (match req.method_ with 225 | "initialize" -> 226 log_debug "Processing initialize request"; 227 Some (handle_initialize req.id req.params) 228 | "tools/list" -> 229 log_debug "Processing tools/list request"; 230 Some (handle_list_tools req.id) 231 | "tools/call" -> 232 log_debug "Processing tools/call request"; 233 Some (handle_call_tool req.id req.params) 234 | "ping" -> 235 log_debug "Processing ping request"; 236 Some (handle_ping req.id) 237 | _ -> 238 log_error (Printf.sprintf "Unknown method received: %s" req.method_); 239 Some (create_error ~id:req.id ~code:(-32601) ~message:("Method not found: " ^ req.method_) ())) 240 | JSONRPCMessage.Notification notif -> 241 log_debug (Printf.sprintf "Received notification with method: %s" notif.method_); 242 (match notif.method_ with 243 | "notifications/initialized" -> 244 log_debug "Client initialization complete - Server is now ready to receive requests"; 245 log_debug (Printf.sprintf "Notification params: %s" 246 (match notif.params with 247 | Some p -> Yojson.Safe.to_string p 248 | None -> "null")); 249 None 250 | _ -> 251 log_debug (Printf.sprintf "Ignoring notification: %s" notif.method_); 252 None) 253 | JSONRPCMessage.Response _ -> 254 log_error "Unexpected response message received"; 255 None 256 | JSONRPCMessage.Error _ -> 257 log_error "Unexpected error message received"; 258 None 259 with 260 | exc -> 261 log_error (Printf.sprintf "Exception during message processing: %s" (Printexc.to_string exc)); 262 log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace())); 263 log_error (Printf.sprintf "Message was: %s" (Yojson.Safe.to_string message)); 264 None 265 266(* Main loop *) 267let rec read_message () = 268 try 269 log_debug "Attempting to read line from stdin..."; 270 let line = read_line () in 271 if line = "" then ( 272 log_debug "Empty line received, ignoring"; 273 None 274 ) else ( 275 log_debug (Printf.sprintf "Raw input: %s" line); 276 try 277 let json = Yojson.Safe.from_string line in 278 log_debug "Successfully parsed JSON"; 279 Some json 280 with 281 | Yojson.Json_error msg -> 282 log_error (Printf.sprintf "Error parsing JSON: %s" msg); 283 log_error (Printf.sprintf "Input was: %s" line); 284 read_message () 285 ) 286 with 287 | End_of_file -> 288 log_debug "End of file received on stdin"; 289 None 290 | Sys_error msg -> 291 log_error (Printf.sprintf "System error while reading: %s" msg); 292 None 293 | exc -> 294 log_error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exc)); 295 None 296 297let () = 298 try 299 (* Enable exception backtraces *) 300 Printexc.record_backtrace true; 301 302 (* Enable line buffering for stdout *) 303 set_binary_mode_out stdout false; 304 305 log_debug "MCP Capitalizer server started"; 306 log_debug (Printf.sprintf "Protocol version: %s" protocol_version); 307 log_debug (Printf.sprintf "Server info: %s v%s" server_info.name server_info.version); 308 309 (* Print environment info for debugging *) 310 log_debug "Environment variables:"; 311 Unix.environment() 312 |> Array.iter (fun s -> 313 try 314 let i = String.index s '=' in 315 let name = String.sub s 0 i in 316 if String.length name > 0 then 317 log_debug (Printf.sprintf " %s" s) 318 with Not_found -> () 319 ); 320 321 let rec server_loop count = 322 log_debug (Printf.sprintf "Waiting for message #%d..." count); 323 match read_message () with 324 | Some json -> 325 log_debug (Printf.sprintf "Received message: %s" (Yojson.Safe.to_string json)); 326 (match process_message json with 327 | Some response -> 328 let response_json = JSONRPCMessage.yojson_of_t response in 329 let response_str = Yojson.Safe.to_string response_json in 330 log_debug (Printf.sprintf "Sending response: %s" response_str); 331 (* Make sure we emit properly formatted JSON on a single line with a newline at the end *) 332 Printf.printf "%s\n" response_str; 333 flush stdout; 334 (* Give the client a moment to process the response *) 335 Unix.sleepf 0.01; 336 server_loop (count + 1) 337 | None -> 338 log_debug "No response needed for this message"; 339 server_loop (count + 1)) 340 | None -> 341 log_debug "End of input stream, terminating server"; 342 () 343 in 344 345 log_debug "Starting server loop..."; 346 log_debug "Waiting for the initialize request..."; 347 348 (* Set up signal handler to gracefully exit *) 349 Sys.(set_signal sigint (Signal_handle (fun _ -> 350 log_debug "Received interrupt signal, exiting..."; 351 exit 0 352 ))); 353 354 server_loop 1; 355 log_debug "Server terminated normally"; 356 with 357 | End_of_file -> 358 log_error "Unexpected end of file"; 359 | Sys_error msg -> 360 log_error (Printf.sprintf "System error: %s" msg); 361 | Unix.Unix_error(err, func, arg) -> 362 log_error (Printf.sprintf "Unix error in %s(%s): %s" func arg (Unix.error_message err)); 363 | exc -> 364 log_error (Printf.sprintf "Unhandled exception: %s" (Printexc.to_string exc)); 365 log_error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()))