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.debugf "Client info: %s v%s"
12 req_data.client_info.name req_data.client_info.version;
13 Log.debugf "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 tools_list = Tool.to_rpc_tools_list (tools server) in
37 let response = Mcp_rpc.ToolsList.create_response ~id:req.id ~tools:tools_list () in
38 Some response
39
40(* Process prompts/list request *)
41let handle_prompts_list server (req:JSONRPCMessage.request) =
42 Log.debug "Processing prompts/list request";
43 let prompts_list = Prompt.to_rpc_prompts_list (prompts server) in
44 let response = Mcp_rpc.PromptsList.create_response ~id:req.id ~prompts:prompts_list () in
45 Some response
46
47(* Process resources/list request *)
48let handle_resources_list server (req:JSONRPCMessage.request) =
49 Log.debug "Processing resources/list request";
50 let resources_list = Resource.to_rpc_resources_list (resources server) in
51 let response = Mcp_rpc.ResourcesList.create_response ~id:req.id ~resources:resources_list () in
52 Some response
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.debugf "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.debugf "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 = ErrorCode.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
84(* Execute a tool *)
85let execute_tool server ctx name args =
86 try
87 let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
88 Log.debugf "Found tool: %s" name;
89
90 (* Call the tool handler *)
91 match tool.handler ctx args with
92 | Ok result ->
93 Log.debug "Tool execution succeeded";
94 result
95 | Error err -> Tool.handle_execution_error err
96 with
97 | Not_found -> Tool.handle_unknown_tool_error name
98 | exn -> Tool.handle_execution_exception exn
99
100(* Convert JSON tool result to RPC content format *)
101let json_to_rpc_content json =
102 match json with
103 | `Assoc fields ->
104 (match List.assoc_opt "content" fields, List.assoc_opt "isError" fields with
105 | Some (`List content_items), Some (`Bool is_error) ->
106 let mcp_content = List.map Mcp.content_of_yojson content_items in
107 let rpc_content = Tool.mcp_content_to_rpc_content mcp_content in
108 (rpc_content, is_error)
109 | _ ->
110 (* Fallback for compatibility with older formats *)
111 let text = Yojson.Safe.to_string json in
112 let text_content = { TextContent.text = text; annotations = None } in
113 ([Mcp_rpc.ToolsCall.ToolContent.Text text_content], false))
114 | _ ->
115 (* Simple fallback for non-object results *)
116 let text = Yojson.Safe.to_string json in
117 let text_content = { TextContent.text = text; annotations = None } in
118 ([Mcp_rpc.ToolsCall.ToolContent.Text text_content], false)
119
120(* Process tools/call request *)
121let handle_tools_call server req =
122 Log.debug "Processing tools/call request";
123 match req.JSONRPCMessage.params with
124 | Some (`Assoc params) ->
125 (match extract_tool_name params with
126 | Some name ->
127 let args = extract_tool_arguments params in
128
129 (* Create context for this request *)
130 let ctx = Context.create
131 ?request_id:(Some req.id)
132 ?progress_token:req.progress_token
133 ~lifespan_context:[("tools/call", `Assoc params)]
134 ()
135 in
136
137 (* Execute the tool *)
138 let result_json = execute_tool server ctx name args in
139
140 (* Convert JSON result to RPC format *)
141 let content, is_error = json_to_rpc_content result_json in
142
143 (* Create the RPC response *)
144 let response = Mcp_rpc.ToolsCall.create_response
145 ~id:req.id
146 ~content
147 ~is_error
148 ()
149 in
150
151 Some response
152 | None ->
153 Some (create_jsonrpc_error req.id InvalidParams "Missing tool name parameter" ()))
154 | _ ->
155 Log.error "Invalid params format for tools/call";
156 Some (create_jsonrpc_error req.id InvalidParams "Invalid params format for tools/call" ())
157
158(* Process ping request *)
159let handle_ping (req:JSONRPCMessage.request) =
160 Log.debug "Processing ping request";
161 Some (create_response ~id:req.JSONRPCMessage.id ~result:(`Assoc []))
162
163(* Handle notifications/initialized *)
164let handle_initialized (notif:JSONRPCMessage.notification) =
165 Log.debug "Client initialization complete - Server is now ready to receive requests";
166 Log.debugf "Notification params: %s"
167 (match notif.JSONRPCMessage.params with
168 | Some p -> Yojson.Safe.to_string p
169 | None -> "null");
170 None
171
172(* Process a single message using the MCP SDK *)
173let process_message server message =
174 try
175 Log.debugf "Processing message: %s" (Yojson.Safe.to_string message);
176 match JSONRPCMessage.t_of_yojson message with
177 | JSONRPCMessage.Request req ->
178 Log.debugf "Received request with method: %s" (Method.to_string req.meth);
179 (match req.meth with
180 | Method.Initialize -> handle_initialize server req
181 | Method.ToolsList -> handle_tools_list server req
182 | Method.ToolsCall -> handle_tools_call server req
183 | Method.PromptsList -> handle_prompts_list server req
184 | Method.ResourcesList -> handle_resources_list server req
185 | _ ->
186 Log.errorf "Unknown method received: %s" (Method.to_string req.meth);
187 Some (create_jsonrpc_error req.id MethodNotFound ("Method not found: " ^ (Method.to_string req.meth)) ()))
188 | JSONRPCMessage.Notification notif ->
189 Log.debugf "Received notification with method: %s" (Method.to_string notif.meth);
190 (match notif.meth with
191 | Method.Initialized -> handle_initialized notif
192 | _ ->
193 Log.debugf "Ignoring notification: %s" (Method.to_string notif.meth);
194 None)
195 | JSONRPCMessage.Response _ ->
196 Log.error "Unexpected response message received";
197 None
198 | JSONRPCMessage.Error _ ->
199 Log.error "Unexpected error message received";
200 None
201 with
202 | Json.Of_json (msg, _) ->
203 Log.errorf "JSON error: %s" msg;
204 (* Can't respond with error because we don't have a request ID *)
205 None
206 | Yojson.Json_error msg ->
207 Log.errorf "JSON parse error: %s" msg;
208 (* Can't respond with error because we don't have a request ID *)
209 None
210 | exc ->
211 Log.errorf "Exception during message processing: %s" (Printexc.to_string exc);
212 Log.errorf "Backtrace: %s" (Printexc.get_backtrace());
213 Log.errorf "Message was: %s" (Yojson.Safe.to_string message);
214 None
215
216(* Extract a request ID from a potentially malformed message *)
217let extract_request_id json =
218 try
219 match json with
220 | `Assoc fields ->
221 (match List.assoc_opt "id" fields with
222 | Some (`Int id) -> Some (`Int id)
223 | Some (`String id) -> Some (`String id)
224 | _ -> None)
225 | _ -> None
226 with _ -> None
227
228(* Handle processing for an input line *)
229let process_input_line server line =
230 if line = "" then (
231 Log.debug "Empty line received, ignoring";
232 None
233 ) else (
234 Log.debugf "Raw input: %s" line;
235 try
236 let json = Yojson.Safe.from_string line in
237 Log.debug "Successfully parsed JSON";
238
239 (* Process the message *)
240 process_message server json
241 with
242 | Yojson.Json_error msg -> begin
243 Log.errorf "Error parsing JSON: %s" msg;
244 Log.errorf "Input was: %s" line;
245 None
246 end
247 )
248
249(* Send a response to the client *)
250let send_response stdout response =
251 let response_json = JSONRPCMessage.yojson_of_t response in
252 let response_str = Yojson.Safe.to_string response_json in
253 Log.debugf "Sending response: %s" response_str;
254
255 (* Write the response followed by a newline *)
256 Eio.Flow.copy_string response_str stdout;
257 Eio.Flow.copy_string "\n" stdout
258
259(* Run the MCP server with the given server configuration *)
260let run_server env server =
261 let stdin = Eio.Stdenv.stdin env in
262 let stdout = Eio.Stdenv.stdout env in
263
264 Log.debugf "Starting MCP server: %s v%s" (name server) (version server);
265 Log.debugf "Protocol version: %s" (protocol_version server);
266
267 (* Enable exception backtraces *)
268 Printexc.record_backtrace true;
269
270 let buf = Eio.Buf_read.of_flow stdin ~initial_size:100 ~max_size:1_000_000 in
271
272 (* Main processing loop *)
273 try
274 while true do
275 Log.debug "Waiting for message...";
276 let line = Eio.Buf_read.line buf in
277
278 (* Process the input and send response if needed *)
279 match process_input_line server line with
280 | Some response -> send_response stdout response
281 | None -> Log.debug "No response needed for this message"
282 done
283 with
284 | End_of_file ->
285 Log.debug "End of file received on stdin";
286 ()
287 | Eio.Exn.Io _ as exn ->
288 Log.errorf "I/O error while reading: %s" (Printexc.to_string exn);
289 ()
290 | exn ->
291 Log.errorf "Exception while reading: %s" (Printexc.to_string exn);
292 ()