Model Context Protocol in OCaml
1open Mcp
2open Jsonrpc
3open Mcp_sdk
4
5(* Process initialize request *)
6let handle_initialize server req =
7 Log.debug "Processing initialize request";
8 let result = match req.JSONRPCMessage.params with
9 | Some params ->
10 let req_data = Initialize.Request.t_of_yojson params in
11 Log.debug (Printf.sprintf "Client info: %s v%s"
12 req_data.client_info.name req_data.client_info.version);
13 Log.debug (Printf.sprintf "Client protocol version: %s" req_data.protocol_version);
14
15 (* Create initialize response *)
16 let result = Initialize.Result.create
17 ~capabilities:(capabilities server)
18 ~server_info:Implementation.{
19 name = name server;
20 version = version server
21 }
22 ~protocol_version:(protocol_version server)
23 ~instructions:(Printf.sprintf "This server provides tools for %s." (name server))
24 ()
25 in
26 Initialize.Result.yojson_of_t result
27 | None ->
28 Log.error "Missing params for initialize request";
29 `Assoc [("error", `String "Missing params for initialize request")]
30 in
31 Some (create_response ~id:req.id ~result)
32
33(* Process tools/list request *)
34let handle_tools_list server (req:JSONRPCMessage.request) =
35 Log.debug "Processing tools/list request";
36 let tool_list = List.map Tool.to_json (tools server) in
37 let result = `Assoc [("tools", `List tool_list)] in
38 Some (create_response ~id:req.id ~result)
39
40(* Process prompts/list request *)
41let handle_prompts_list server (req:JSONRPCMessage.request) =
42 Log.debug "Processing prompts/list request";
43 let prompt_list = List.map Prompt.to_json (prompts server) in
44 let result = `Assoc [("prompts", `List prompt_list)] in
45 Some (create_response ~id:req.id ~result)
46
47(* Process resources/list request *)
48let handle_resources_list server (req:JSONRPCMessage.request) =
49 Log.debug "Processing resources/list request";
50 let resource_list = List.map Resource.to_json (resources server) in
51 let result = `Assoc [("resources", `List resource_list)] in
52 Some (create_response ~id:req.id ~result)
53
54(* Extract the tool name from params *)
55let extract_tool_name params =
56 match List.assoc_opt "name" params with
57 | Some (`String name) ->
58 Log.debug (Printf.sprintf "Tool name: %s" name);
59 Some name
60 | _ ->
61 Log.error "Missing or invalid 'name' parameter in tool call";
62 None
63
64(* Extract the tool arguments from params *)
65let extract_tool_arguments params =
66 match List.assoc_opt "arguments" params with
67 | Some (args) ->
68 Log.debug (Printf.sprintf "Tool arguments: %s" (Yojson.Safe.to_string args));
69 args
70 | _ ->
71 Log.debug "No arguments provided for tool call, using empty object";
72 `Assoc [] (* Empty arguments is valid *)
73
74(* Create a proper JSONRPC error with code and data *)
75let create_jsonrpc_error id code message ?data () =
76 let error_code = error_code_to_int code in
77 let error_data = match data with
78 | Some d -> d
79 | None -> `Null
80 in
81 create_error ~id ~code:error_code ~message ~data:(Some error_data) ()
82
83(* Create a tool error result with structured content *)
84let create_tool_error_result error =
85 create_tool_result [TextContent error] ~is_error:true
86
87(* Handle tool execution errors *)
88let handle_tool_execution_error err =
89 Log.error (Printf.sprintf "Tool execution failed: %s" err);
90 create_tool_error_result (Printf.sprintf "Error executing tool: %s" err)
91
92(* Handle unknown tool error *)
93let handle_unknown_tool_error name =
94 Log.error (Printf.sprintf "Unknown tool: %s" name);
95 create_tool_error_result (Printf.sprintf "Unknown tool: %s" name)
96
97(* Handle general tool execution exception *)
98let handle_tool_execution_exception exn =
99 Log.error (Printf.sprintf "Exception executing tool: %s" (Printexc.to_string exn));
100 create_tool_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn))
101
102(* Execute a tool *)
103let execute_tool server ctx name args =
104 try
105 let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
106 Log.debug (Printf.sprintf "Found tool: %s" name);
107
108 (* Call the tool handler *)
109 match tool.handler ctx args with
110 | Ok result ->
111 Log.debug "Tool execution succeeded";
112 result
113 | Error err -> handle_tool_execution_error err
114 with
115 | Not_found -> handle_unknown_tool_error name
116 | exn -> handle_tool_execution_exception exn
117
118(* Process tools/call request *)
119let handle_tools_call server req =
120 Log.debug "Processing tools/call request";
121 match req.JSONRPCMessage.params with
122 | Some (`Assoc params) ->
123 (match extract_tool_name params with
124 | Some name ->
125 let args = extract_tool_arguments params in
126
127 (* Create context for this request *)
128 let ctx = Context.create
129 ?request_id:(Some req.id) (* Store request ID for progress reporting *)
130 ~lifespan_context:[("tools/call", `Assoc params)] (* Store params for reference *)
131 ()
132 in
133
134 (* Set progress token if present *)
135 ctx.progress_token <- req.progress_token;
136
137 (* Execute the tool *)
138 let result = execute_tool server ctx name args in
139
140 (* Process progress messages if any *)
141 let progress_msg = Context.report_progress ctx 1.0 1.0 in
142 (match progress_msg with
143 | Some msg -> Log.debug "Progress complete notification would be sent here";
144 | None -> ());
145
146 Some (create_response ~id:req.id ~result)
147 | None ->
148 Some (create_jsonrpc_error req.id InvalidParams "Missing tool name parameter" ()))
149 | _ ->
150 Log.error "Invalid params format for tools/call";
151 Some (create_jsonrpc_error req.id InvalidParams "Invalid params format for tools/call" ())
152
153(* Process ping request *)
154let handle_ping (req:JSONRPCMessage.request) =
155 Log.debug "Processing ping request";
156 Some (create_response ~id:req.JSONRPCMessage.id ~result:(`Assoc []))
157
158(* Handle notifications/initialized *)
159let handle_initialized (notif:JSONRPCMessage.notification) =
160 Log.debug "Client initialization complete - Server is now ready to receive requests";
161 Log.debug (Printf.sprintf "Notification params: %s"
162 (match notif.JSONRPCMessage.params with
163 | Some p -> Yojson.Safe.to_string p
164 | None -> "null"));
165 None
166
167(* Process a single message using the MCP SDK *)
168let process_message server message =
169 try
170 Log.debug (Printf.sprintf "Processing message: %s" (Yojson.Safe.to_string message));
171 match JSONRPCMessage.t_of_yojson message with
172 | JSONRPCMessage.Request req ->
173 Log.debug (Printf.sprintf "Received request with method: %s" req.method_);
174 (match req.method_ with
175 | "initialize" -> handle_initialize server req
176 | "tools/list" -> handle_tools_list server req
177 | "tools/call" -> handle_tools_call server req
178 | "prompts/list" -> handle_prompts_list server req
179 | "resources/list" -> handle_resources_list server req
180 | "ping" -> handle_ping req
181 | _ ->
182 Log.error (Printf.sprintf "Unknown method received: %s" req.method_);
183 Some (create_jsonrpc_error req.id MethodNotFound ("Method not found: " ^ req.method_) ()))
184 | JSONRPCMessage.Notification notif ->
185 Log.debug (Printf.sprintf "Received notification with method: %s" notif.method_);
186 (match notif.method_ with
187 | "notifications/initialized" -> handle_initialized notif
188 | _ ->
189 Log.debug (Printf.sprintf "Ignoring notification: %s" notif.method_);
190 None)
191 | JSONRPCMessage.Response _ ->
192 Log.error "Unexpected response message received";
193 None
194 | JSONRPCMessage.Error _ ->
195 Log.error "Unexpected error message received";
196 None
197 with
198 | Json.Of_json (msg, _) ->
199 Log.error (Printf.sprintf "JSON error: %s" msg);
200 (* Can't respond with error because we don't have a request ID *)
201 None
202 | Yojson.Json_error msg ->
203 Log.error (Printf.sprintf "JSON parse error: %s" msg);
204 (* Can't respond with error because we don't have a request ID *)
205 None
206 | exc ->
207 Log.error (Printf.sprintf "Exception during message processing: %s" (Printexc.to_string exc));
208 Log.error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
209 Log.error (Printf.sprintf "Message was: %s" (Yojson.Safe.to_string message));
210 None
211
212(* Extract a request ID from a potentially malformed message *)
213let extract_request_id json =
214 try
215 match json with
216 | `Assoc fields ->
217 (match List.assoc_opt "id" fields with
218 | Some (`Int id) -> Some (`Int id)
219 | Some (`String id) -> Some (`String id)
220 | _ -> None)
221 | _ -> None
222 with _ -> None
223
224(* Handle processing for an input line *)
225let process_input_line server line =
226 if line = "" then (
227 Log.debug "Empty line received, ignoring";
228 None
229 ) else (
230 Log.debug (Printf.sprintf "Raw input: %s" line);
231 try
232 let json = Yojson.Safe.from_string line in
233 Log.debug "Successfully parsed JSON";
234
235 (* Process the message *)
236 process_message server json
237 with
238 | Yojson.Json_error msg -> begin
239 Log.error (Printf.sprintf "Error parsing JSON: %s" msg);
240 Log.error (Printf.sprintf "Input was: %s" line);
241 None
242 end
243 )
244
245(* Send a response to the client *)
246let send_response stdout response =
247 let response_json = JSONRPCMessage.yojson_of_t response in
248 let response_str = Yojson.Safe.to_string response_json in
249 Log.debug (Printf.sprintf "Sending response: %s" response_str);
250
251 (* Write the response followed by a newline *)
252 Eio.Flow.copy_string response_str stdout;
253 Eio.Flow.copy_string "\n" stdout
254
255(* Run the MCP server with the given server configuration *)
256let run_server env server =
257 let stdin = Eio.Stdenv.stdin env in
258 let stdout = Eio.Stdenv.stdout env in
259
260 Log.debug (Printf.sprintf "Starting MCP server: %s v%s" (name server) (version server));
261 Log.debug (Printf.sprintf "Protocol version: %s" (protocol_version server));
262
263 (* Enable exception backtraces *)
264 Printexc.record_backtrace true;
265
266 let buf = Eio.Buf_read.of_flow stdin ~initial_size:100 ~max_size:1_000_000 in
267
268 (* Main processing loop *)
269 try
270 while true do
271 Log.debug "Waiting for message...";
272 let line = Eio.Buf_read.line buf in
273
274 (* Process the input and send response if needed *)
275 match process_input_line server line with
276 | Some response -> send_response stdout response
277 | None -> Log.debug "No response needed for this message"
278 done
279 with
280 | End_of_file ->
281 Log.debug "End of file received on stdin";
282 ()
283 | Eio.Exn.Io _ as exn ->
284 Log.error (Printf.sprintf "I/O error while reading: %s" (Printexc.to_string exn));
285 ()
286 | exn ->
287 Log.error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exn));
288 ()