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