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(* Create an error result with text content *)
55let create_error_content err =
56 let error_content = TextContent.{
57 text = err;
58 annotations = None
59 } in
60 `Assoc [
61 ("content", `List [TextContent.yojson_of_t error_content]);
62 ("isError", `Bool true)
63 ]
64
65(* Extract the tool name from params *)
66let extract_tool_name params =
67 match List.assoc_opt "name" params with
68 | Some (`String name) ->
69 Log.debug (Printf.sprintf "Tool name: %s" name);
70 Some name
71 | _ ->
72 Log.error "Missing or invalid 'name' parameter in tool call";
73 None
74
75(* Extract the tool arguments from params *)
76let extract_tool_arguments params =
77 match List.assoc_opt "arguments" params with
78 | Some (args) ->
79 Log.debug (Printf.sprintf "Tool arguments: %s" (Yojson.Safe.to_string args));
80 args
81 | _ ->
82 Log.debug "No arguments provided for tool call, using empty object";
83 `Assoc [] (* Empty arguments is valid *)
84
85(* Handle tool execution errors *)
86let handle_tool_execution_error err =
87 Log.error (Printf.sprintf "Tool execution failed: %s" err);
88 create_error_content (Printf.sprintf "Error executing tool: %s" err)
89
90(* Handle unknown tool error *)
91let handle_unknown_tool_error name =
92 Log.error (Printf.sprintf "Unknown tool: %s" name);
93 create_error_content (Printf.sprintf "Unknown tool: %s" name)
94
95(* Handle general tool execution exception *)
96let handle_tool_execution_exception exn =
97 Log.error (Printf.sprintf "Exception executing tool: %s" (Printexc.to_string exn));
98 create_error_content (Printf.sprintf "Internal error: %s" (Printexc.to_string exn))
99
100(* Execute a tool *)
101let execute_tool server ctx name args =
102 try
103 let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
104 Log.debug (Printf.sprintf "Found tool: %s" name);
105
106 (* Call the tool handler *)
107 match tool.handler ctx args with
108 | Ok result ->
109 Log.debug "Tool execution succeeded";
110 result
111 | Error err -> handle_tool_execution_error err
112 with
113 | Not_found -> handle_unknown_tool_error name
114 | exn -> handle_tool_execution_exception exn
115
116(* Process tools/call request *)
117let handle_tools_call server req =
118 Log.debug "Processing tools/call request";
119 match req.JSONRPCMessage.params with
120 | Some (`Assoc params) ->
121 (match extract_tool_name params with
122 | Some name ->
123 let args = extract_tool_arguments params in
124
125 (* Create context for this request *)
126 let ctx = Context.create ?request_id:req.progress_token () in
127
128 (* Execute the tool *)
129 let result = execute_tool server ctx name args in
130 Some (create_response ~id:req.id ~result)
131 | None ->
132 Some (create_error ~id:req.id ~code:(-32602) ~message:"Missing tool name parameter" ()))
133 | _ ->
134 Log.error "Invalid params format for tools/call";
135 Some (create_error ~id:req.id ~code:(-32602) ~message:"Invalid params for tools/call" ())
136
137(* Process ping request *)
138let handle_ping (req:JSONRPCMessage.request) =
139 Log.debug "Processing ping request";
140 Some (create_response ~id:req.JSONRPCMessage.id ~result:(`Assoc []))
141
142(* Handle notifications/initialized *)
143let handle_initialized (notif:JSONRPCMessage.notification) =
144 Log.debug "Client initialization complete - Server is now ready to receive requests";
145 Log.debug (Printf.sprintf "Notification params: %s"
146 (match notif.JSONRPCMessage.params with
147 | Some p -> Yojson.Safe.to_string p
148 | None -> "null"));
149 None
150
151(* Process a single message using the MCP SDK *)
152let process_message server message =
153 try
154 Log.debug (Printf.sprintf "Processing message: %s" (Yojson.Safe.to_string message));
155 match JSONRPCMessage.t_of_yojson message with
156 | JSONRPCMessage.Request req ->
157 Log.debug (Printf.sprintf "Received request with method: %s" req.method_);
158 (match req.method_ with
159 | "initialize" -> handle_initialize server req
160 | "tools/list" -> handle_tools_list server req
161 | "tools/call" -> handle_tools_call server req
162 | "prompts/list" -> handle_prompts_list server req
163 | "resources/list" -> handle_resources_list server req
164 | "ping" -> handle_ping req
165 | _ ->
166 Log.error (Printf.sprintf "Unknown method received: %s" req.method_);
167 Some (create_error ~id:req.id ~code:(-32601) ~message:("Method not found: " ^ req.method_) ()))
168 | JSONRPCMessage.Notification notif ->
169 Log.debug (Printf.sprintf "Received notification with method: %s" notif.method_);
170 (match notif.method_ with
171 | "notifications/initialized" -> handle_initialized notif
172 | _ ->
173 Log.debug (Printf.sprintf "Ignoring notification: %s" notif.method_);
174 None)
175 | JSONRPCMessage.Response _ ->
176 Log.error "Unexpected response message received";
177 None
178 | JSONRPCMessage.Error _ ->
179 Log.error "Unexpected error message received";
180 None
181 with
182 | Json.Of_json (msg, _) ->
183 Log.error (Printf.sprintf "JSON error: %s" msg);
184 None
185 | exc ->
186 Log.error (Printf.sprintf "Exception during message processing: %s" (Printexc.to_string exc));
187 Log.error (Printf.sprintf "Backtrace: %s" (Printexc.get_backtrace()));
188 Log.error (Printf.sprintf "Message was: %s" (Yojson.Safe.to_string message));
189 None
190
191(* Handle processing for an input line *)
192let process_input_line server line =
193 if line = "" then (
194 Log.debug "Empty line received, ignoring";
195 None
196 ) else (
197 Log.debug (Printf.sprintf "Raw input: %s" line);
198 try
199 let json = Yojson.Safe.from_string line in
200 Log.debug "Successfully parsed JSON";
201
202 (* Process the message *)
203 process_message server json
204 with
205 | Yojson.Json_error msg -> begin
206 Log.error (Printf.sprintf "Error parsing JSON: %s" msg);
207 Log.error (Printf.sprintf "Input was: %s" line);
208 None
209 end
210 )
211
212(* Send a response to the client *)
213let send_response stdout response =
214 let response_json = JSONRPCMessage.yojson_of_t response in
215 let response_str = Yojson.Safe.to_string response_json in
216 Log.debug (Printf.sprintf "Sending response: %s" response_str);
217
218 (* Write the response followed by a newline *)
219 Eio.Flow.copy_string response_str stdout;
220 Eio.Flow.copy_string "\n" stdout
221
222(* Run the MCP server with the given server configuration *)
223let run_server env server =
224 let stdin = Eio.Stdenv.stdin env in
225 let stdout = Eio.Stdenv.stdout env in
226
227 Log.debug (Printf.sprintf "Starting MCP server: %s v%s" (name server) (version server));
228 Log.debug (Printf.sprintf "Protocol version: %s" (protocol_version server));
229
230 (* Enable exception backtraces *)
231 Printexc.record_backtrace true;
232
233 let buf = Eio.Buf_read.of_flow stdin ~initial_size:100 ~max_size:1_000_000 in
234
235 (* Main processing loop *)
236 try
237 while true do
238 Log.debug "Waiting for message...";
239 let line = Eio.Buf_read.line buf in
240
241 (* Process the input and send response if needed *)
242 match process_input_line server line with
243 | Some response -> send_response stdout response
244 | None -> Log.debug "No response needed for this message"
245 done
246 with
247 | End_of_file ->
248 Log.debug "End of file received on stdin";
249 ()
250 | Eio.Exn.Io _ as exn ->
251 Log.error (Printf.sprintf "I/O error while reading: %s" (Printexc.to_string exn));
252 ()
253 | exn ->
254 Log.error (Printf.sprintf "Exception while reading: %s" (Printexc.to_string exn));
255 ()