Model Context Protocol in OCaml
1open Mcp
2open Jsonrpc
3open Mcp_sdk
4
5(* Create a proper JSONRPC error with code and data *)
6let create_jsonrpc_error id code message ?data () =
7 let error_code = ErrorCode.to_int code in
8 let error_data = match data with
9 | Some d -> d
10 | None -> `Null
11 in
12 create_error ~id ~code:error_code ~message ~data:(Some error_data) ()
13
14(* Process initialize request *)
15let handle_initialize server req =
16 Log.debug "Processing initialize request";
17 let result = match req.JSONRPCMessage.params with
18 | Some params ->
19 let req_data = Initialize.Request.t_of_yojson params in
20 Log.debugf "Client info: %s v%s"
21 req_data.client_info.name req_data.client_info.version;
22 Log.debugf "Client protocol version: %s" req_data.protocol_version;
23
24 (* Create initialize response *)
25 let result = Initialize.Result.create
26 ~capabilities:(capabilities server)
27 ~server_info:Implementation.{
28 name = name server;
29 version = version server
30 }
31 ~protocol_version:(protocol_version server)
32 ~instructions:(Printf.sprintf "This server provides tools for %s." (name server))
33 ()
34 in
35 Initialize.Result.yojson_of_t result
36 | None ->
37 Log.error "Missing params for initialize request";
38 `Assoc [("error", `String "Missing params for initialize request")]
39 in
40 Some (create_response ~id:req.id ~result)
41
42(* Process tools/list request *)
43let handle_tools_list server (req:JSONRPCMessage.request) =
44 Log.debug "Processing tools/list request";
45 let tools_list = Tool.to_rpc_tools_list (tools server) in
46 let response = Mcp_rpc.ToolsList.create_response ~id:req.id ~tools:tools_list () in
47 Some response
48
49(* Process prompts/list request *)
50let handle_prompts_list server (req:JSONRPCMessage.request) =
51 Log.debug "Processing prompts/list request";
52 let prompts_list = Prompt.to_rpc_prompts_list (prompts server) in
53 let response = Mcp_rpc.PromptsList.create_response ~id:req.id ~prompts:prompts_list () in
54 Some response
55
56(* Process resources/list request *)
57let handle_resources_list server (req:JSONRPCMessage.request) =
58 Log.debug "Processing resources/list request";
59 let resources_list = Resource.to_rpc_resources_list (resources server) in
60 let response = Mcp_rpc.ResourcesList.create_response ~id:req.id ~resources:resources_list () in
61 Some response
62
63(* Process resources/templates/list request *)
64let handle_resource_templates_list server (req:JSONRPCMessage.request) =
65 Log.debug "Processing resources/templates/list request";
66 let templates_list = ResourceTemplate.to_rpc_resource_templates_list (resource_templates server) in
67 let response = Mcp_rpc.ListResourceTemplatesResult.create_response ~id:req.id ~resource_templates:templates_list () in
68 Some response
69
70(* Utility module for resource template matching *)
71module Resource_matcher = struct
72 (* Define variants for resource handling result *)
73 type resource_match =
74 | DirectResource of Resource.t * string list
75 | TemplateResource of ResourceTemplate.t * string list
76 | NoMatch
77
78 (* Extract parameters from a template URI *)
79 let extract_template_vars template_uri uri =
80 (* Simple template variable extraction - could be enhanced with regex *)
81 let template_parts = String.split_on_char '/' template_uri in
82 let uri_parts = String.split_on_char '/' uri in
83
84 if List.length template_parts <> List.length uri_parts then
85 None
86 else
87 (* Match parts and extract variables *)
88 let rec match_parts tparts uparts acc =
89 match tparts, uparts with
90 | [], [] -> Some (List.rev acc)
91 | th::tt, uh::ut ->
92 (* Check if this part is a template variable *)
93 if String.length th > 2 &&
94 String.get th 0 = '{' &&
95 String.get th (String.length th - 1) = '}' then
96 (* Extract variable value and continue *)
97 match_parts tt ut (uh::acc)
98 else if th = uh then
99 (* Fixed part matches, continue *)
100 match_parts tt ut acc
101 else
102 (* Fixed part doesn't match, fail *)
103 None
104 | _, _ -> None
105 in
106 match_parts template_parts uri_parts []
107
108 (* Find a matching resource or template for a URI *)
109 let find_match server uri =
110 (* Try direct resource match first *)
111 match List.find_opt (fun resource -> resource.Resource.uri = uri) (resources server) with
112 | Some resource -> DirectResource (resource, [])
113 | None ->
114 (* Try template match next *)
115 let templates = resource_templates server in
116
117 (* Try each template to see if it matches *)
118 let rec try_templates templates =
119 match templates with
120 | [] -> NoMatch
121 | template::rest ->
122 match extract_template_vars template.ResourceTemplate.uri_template uri with
123 | Some params -> TemplateResource (template, params)
124 | None -> try_templates rest
125 in
126 try_templates templates
127end
128
129(* Process resources/read request *)
130let handle_resources_read server (req:JSONRPCMessage.request) =
131 Log.debug "Processing resources/read request";
132 match req.JSONRPCMessage.params with
133 | None ->
134 Log.error "Missing params for resources/read request";
135 Some (create_jsonrpc_error req.id ErrorCode.InvalidParams "Missing params for resources/read request" ())
136 | Some params ->
137 let req_data = Mcp_rpc.ResourcesRead.Request.t_of_yojson params in
138 let uri = req_data.uri in
139 Log.debugf "Resource URI: %s" uri;
140
141 (* Find matching resource or template *)
142 match Resource_matcher.find_match server uri with
143 | Resource_matcher.DirectResource (resource, params) ->
144 (* Create context for this request *)
145 let ctx = Context.create
146 ?request_id:(Some req.id)
147 ?progress_token:req.progress_token
148 ~lifespan_context:[("resources/read", `Assoc [("uri", `String uri)])]
149 ()
150 in
151
152 Log.debugf "Handling direct resource: %s" resource.name;
153
154 (* Call the resource handler *)
155 (match resource.handler ctx params with
156 | Ok content ->
157 (* Create text resource content *)
158 let mime_type = match resource.mime_type with
159 | Some mime -> mime
160 | None -> "text/plain"
161 in
162 let text_resource = {
163 TextResourceContents.uri;
164 text = content;
165 mime_type = Some mime_type
166 } in
167 let resource_content = Mcp_rpc.ResourcesRead.ResourceContent.TextResource text_resource in
168 let response = Mcp_rpc.ResourcesRead.create_response ~id:req.id ~contents:[resource_content] () in
169 Some response
170 | Error err ->
171 Log.errorf "Error reading resource: %s" err;
172 Some (create_jsonrpc_error req.id ErrorCode.InternalError ("Error reading resource: " ^ err) ()))
173
174 | Resource_matcher.TemplateResource (template, params) ->
175 (* Create context for this request *)
176 let ctx = Context.create
177 ?request_id:(Some req.id)
178 ?progress_token:req.progress_token
179 ~lifespan_context:[("resources/read", `Assoc [("uri", `String uri)])]
180 ()
181 in
182
183 Log.debugf "Handling resource template: %s with params: [%s]"
184 template.name
185 (String.concat ", " params);
186
187 (* Call the template handler *)
188 (match template.handler ctx params with
189 | Ok content ->
190 (* Create text resource content *)
191 let mime_type = match template.mime_type with
192 | Some mime -> mime
193 | None -> "text/plain"
194 in
195 let text_resource = {
196 TextResourceContents.uri;
197 text = content;
198 mime_type = Some mime_type
199 } in
200 let resource_content = Mcp_rpc.ResourcesRead.ResourceContent.TextResource text_resource in
201 let response = Mcp_rpc.ResourcesRead.create_response ~id:req.id ~contents:[resource_content] () in
202 Some response
203 | Error err ->
204 Log.errorf "Error reading resource template: %s" err;
205 Some (create_jsonrpc_error req.id ErrorCode.InternalError ("Error reading resource template: " ^ err) ()))
206
207 | Resource_matcher.NoMatch ->
208 Log.errorf "Resource not found: %s" uri;
209 Some (create_jsonrpc_error req.id ErrorCode.InvalidParams ("Resource not found: " ^ uri) ())
210
211(* Extract the tool name from params *)
212let extract_tool_name params =
213 match List.assoc_opt "name" params with
214 | Some (`String name) ->
215 Log.debugf "Tool name: %s" name;
216 Some name
217 | _ ->
218 Log.error "Missing or invalid 'name' parameter in tool call";
219 None
220
221(* Extract the tool arguments from params *)
222let extract_tool_arguments params =
223 match List.assoc_opt "arguments" params with
224 | Some (args) ->
225 Log.debugf "Tool arguments: %s" (Yojson.Safe.to_string args);
226 args
227 | _ ->
228 Log.debug "No arguments provided for tool call, using empty object";
229 `Assoc [] (* Empty arguments is valid *)
230
231(* Execute a tool *)
232let execute_tool server ctx name args =
233 try
234 let tool = List.find (fun t -> t.Tool.name = name) (tools server) in
235 Log.debugf "Found tool: %s" name;
236
237 (* Call the tool handler *)
238 match tool.handler ctx args with
239 | Ok result ->
240 Log.debug "Tool execution succeeded";
241 result
242 | Error err -> Tool.handle_execution_error err
243 with
244 | Not_found -> Tool.handle_unknown_tool_error name
245 | exn -> Tool.handle_execution_exception exn
246
247(* Convert JSON tool result to RPC content format *)
248let json_to_rpc_content json =
249 match json with
250 | `Assoc fields ->
251 (match List.assoc_opt "content" fields, List.assoc_opt "isError" fields with
252 | Some (`List content_items), Some (`Bool is_error) ->
253 let mcp_content = List.map Mcp.content_of_yojson content_items in
254 let rpc_content = Tool.mcp_content_to_rpc_content mcp_content in
255 (rpc_content, is_error)
256 | _ ->
257 (* Fallback for compatibility with older formats *)
258 let text = Yojson.Safe.to_string json in
259 let text_content = { TextContent.text = text; annotations = None } in
260 ([Mcp_rpc.ToolsCall.ToolContent.Text text_content], false))
261 | _ ->
262 (* Simple fallback for non-object results *)
263 let text = Yojson.Safe.to_string json in
264 let text_content = { TextContent.text = text; annotations = None } in
265 ([Mcp_rpc.ToolsCall.ToolContent.Text text_content], false)
266
267(* Process tools/call request *)
268let handle_tools_call server req =
269 Log.debug "Processing tools/call request";
270 match req.JSONRPCMessage.params with
271 | Some (`Assoc params) ->
272 (match extract_tool_name params with
273 | Some name ->
274 let args = extract_tool_arguments params in
275
276 (* Create context for this request *)
277 let ctx = Context.create
278 ?request_id:(Some req.id)
279 ?progress_token:req.progress_token
280 ~lifespan_context:[("tools/call", `Assoc params)]
281 ()
282 in
283
284 (* Execute the tool *)
285 let result_json = execute_tool server ctx name args in
286
287 (* Convert JSON result to RPC format *)
288 let content, is_error = json_to_rpc_content result_json in
289
290 (* Create the RPC response *)
291 let response = Mcp_rpc.ToolsCall.create_response
292 ~id:req.id
293 ~content
294 ~is_error
295 ()
296 in
297
298 Some response
299 | None ->
300 Some (create_jsonrpc_error req.id InvalidParams "Missing tool name parameter" ()))
301 | _ ->
302 Log.error "Invalid params format for tools/call";
303 Some (create_jsonrpc_error req.id InvalidParams "Invalid params format for tools/call" ())
304
305(* Process ping request *)
306let handle_ping (req:JSONRPCMessage.request) =
307 Log.debug "Processing ping request";
308 Some (create_response ~id:req.JSONRPCMessage.id ~result:(`Assoc []))
309
310(* Handle notifications/initialized *)
311let handle_initialized (notif:JSONRPCMessage.notification) =
312 Log.debug "Client initialization complete - Server is now ready to receive requests";
313 Log.debugf "Notification params: %s"
314 (match notif.JSONRPCMessage.params with
315 | Some p -> Yojson.Safe.to_string p
316 | None -> "null");
317 None
318
319(* Process a single message using the MCP SDK *)
320let process_message server message =
321 try
322 Log.debugf "Processing message: %s" (Yojson.Safe.to_string message);
323 match JSONRPCMessage.t_of_yojson message with
324 | JSONRPCMessage.Request req ->
325 Log.debugf "Received request with method: %s" (Method.to_string req.meth);
326 (match req.meth with
327 | Method.Initialize -> handle_initialize server req
328 | Method.ToolsList -> handle_tools_list server req
329 | Method.ToolsCall -> handle_tools_call server req
330 | Method.PromptsList -> handle_prompts_list server req
331 | Method.ResourcesList -> handle_resources_list server req
332 | Method.ResourcesRead -> handle_resources_read server req
333 | Method.ResourceTemplatesList -> handle_resource_templates_list server req
334 | _ ->
335 Log.errorf "Unknown method received: %s" (Method.to_string req.meth);
336 Some (create_jsonrpc_error req.id ErrorCode.MethodNotFound ("Method not found: " ^ (Method.to_string req.meth)) ()))
337 | JSONRPCMessage.Notification notif ->
338 Log.debugf "Received notification with method: %s" (Method.to_string notif.meth);
339 (match notif.meth with
340 | Method.Initialized -> handle_initialized notif
341 | _ ->
342 Log.debugf "Ignoring notification: %s" (Method.to_string notif.meth);
343 None)
344 | JSONRPCMessage.Response _ ->
345 Log.error "Unexpected response message received";
346 None
347 | JSONRPCMessage.Error _ ->
348 Log.error "Unexpected error message received";
349 None
350 with
351 | Json.Of_json (msg, _) ->
352 Log.errorf "JSON error: %s" msg;
353 (* Can't respond with error because we don't have a request ID *)
354 None
355 | Yojson.Json_error msg ->
356 Log.errorf "JSON parse error: %s" msg;
357 (* Can't respond with error because we don't have a request ID *)
358 None
359 | exc ->
360 Log.errorf "Exception during message processing: %s" (Printexc.to_string exc);
361 Log.errorf "Backtrace: %s" (Printexc.get_backtrace());
362 Log.errorf "Message was: %s" (Yojson.Safe.to_string message);
363 None
364
365(* Extract a request ID from a potentially malformed message *)
366let extract_request_id json =
367 try
368 match json with
369 | `Assoc fields ->
370 (match List.assoc_opt "id" fields with
371 | Some (`Int id) -> Some (`Int id)
372 | Some (`String id) -> Some (`String id)
373 | _ -> None)
374 | _ -> None
375 with _ -> None
376
377(* Handle processing for an input line *)
378let process_input_line server line =
379 if line = "" then (
380 Log.debug "Empty line received, ignoring";
381 None
382 ) else (
383 Log.debugf "Raw input: %s" line;
384 try
385 let json = Yojson.Safe.from_string line in
386 Log.debug "Successfully parsed JSON";
387
388 (* Process the message *)
389 process_message server json
390 with
391 | Yojson.Json_error msg -> begin
392 Log.errorf "Error parsing JSON: %s" msg;
393 Log.errorf "Input was: %s" line;
394 None
395 end
396 )
397
398(* Send a response to the client *)
399let send_response stdout response =
400 let response_json = JSONRPCMessage.yojson_of_t response in
401 let response_str = Yojson.Safe.to_string response_json in
402 Log.debugf "Sending response: %s" response_str;
403
404 (* Write the response followed by a newline *)
405 Eio.Flow.copy_string response_str stdout;
406 Eio.Flow.copy_string "\n" stdout
407
408(* Run the MCP server with the given server configuration *)
409let run_server env server =
410 let stdin = Eio.Stdenv.stdin env in
411 let stdout = Eio.Stdenv.stdout env in
412
413 Log.debugf "Starting MCP server: %s v%s" (name server) (version server);
414 Log.debugf "Protocol version: %s" (protocol_version server);
415
416 (* Enable exception backtraces *)
417 Printexc.record_backtrace true;
418
419 let buf = Eio.Buf_read.of_flow stdin ~initial_size:100 ~max_size:1_000_000 in
420
421 (* Main processing loop *)
422 try
423 while true do
424 Log.debug "Waiting for message...";
425 let line = Eio.Buf_read.line buf in
426
427 (* Process the input and send response if needed *)
428 match process_input_line server line with
429 | Some response -> send_response stdout response
430 | None -> Log.debug "No response needed for this message"
431 done
432 with
433 | End_of_file ->
434 Log.debug "End of file received on stdin";
435 ()
436 | Eio.Exn.Io _ as exn ->
437 Log.errorf "I/O error while reading: %s" (Printexc.to_string exn);
438 ()
439 | exn ->
440 Log.errorf "Exception while reading: %s" (Printexc.to_string exn);
441 ()