Model Context Protocol in OCaml
1open Mcp 2open Jsonrpc 3 4(* SDK version *) 5let version = "0.1.0" 6 7(* Logging utilities *) 8module Log = struct 9 type level = Debug | Info | Warning | Error 10 11 let string_of_level = function 12 | Debug -> "DEBUG" 13 | Info -> "INFO" 14 | Warning -> "WARNING" 15 | Error -> "ERROR" 16 17 let logf level fmt = 18 Printf.fprintf stderr "[%s] " (string_of_level level); 19 Printf.kfprintf 20 (fun oc -> 21 Printf.fprintf oc "\n"; 22 flush oc) 23 stderr fmt 24 25 let debugf fmt = logf Debug fmt 26 let infof fmt = logf Info fmt 27 let warningf fmt = logf Warning fmt 28 let errorf fmt = logf Error fmt 29 30 (* Backward compatibility functions that take a simple string *) 31 let log level msg = logf level "%s" msg 32 let debug msg = debugf "%s" msg 33 let info msg = infof "%s" msg 34 let warning msg = warningf "%s" msg 35 let error msg = errorf "%s" msg 36end 37 38(* Context for tools and resources *) 39module Context = struct 40 type t = { 41 request_id : RequestId.t option; 42 lifespan_context : (string * Json.t) list; 43 progress_token : ProgressToken.t option; 44 } 45 46 let create ?request_id ?progress_token ?(lifespan_context = []) () = 47 { request_id; lifespan_context; progress_token } 48 49 let get_context_value ctx key = List.assoc_opt key ctx.lifespan_context 50 51 let report_progress ctx value total = 52 match (ctx.progress_token, ctx.request_id) with 53 | Some token, Some _id -> 54 let params = 55 `Assoc 56 [ 57 ("progress", `Float value); 58 ("total", `Float total); 59 ("progressToken", ProgressToken.yojson_of_t token); 60 ] 61 in 62 Some 63 (create_notification ~meth:Method.Progress ~params:(Some params) ()) 64 | _ -> None 65end 66 67(* Tools for the MCP server *) 68module Tool = struct 69 type handler = Context.t -> Json.t -> (Json.t, string) result 70 71 type t = { 72 name : string; 73 description : string option; 74 input_schema : Json.t; (* JSON Schema *) 75 handler : handler; 76 } 77 78 let create ~name ?description ~input_schema ~handler () = 79 { name; description; input_schema; handler } 80 81 let to_json tool = 82 let assoc = 83 [ ("name", `String tool.name); ("inputSchema", tool.input_schema) ] 84 in 85 let assoc = 86 match tool.description with 87 | Some desc -> ("description", `String desc) :: assoc 88 | None -> assoc 89 in 90 `Assoc assoc 91 92 (* Convert to Mcp_rpc.ToolsList.Tool.t *) 93 let to_rpc_tool_list_tool (tool : t) = 94 Mcp_rpc.ToolsList.Tool. 95 { 96 name = tool.name; 97 description = tool.description; 98 input_schema = tool.input_schema; 99 annotations = None; 100 (* Could be extended to support annotations *) 101 } 102 103 (* Convert a list of Tool.t to the format needed for tools/list response *) 104 let to_rpc_tools_list tools = List.map to_rpc_tool_list_tool tools 105 106 (* Convert Mcp_rpc.ToolsCall response content to Mcp.content list *) 107 let rpc_content_to_mcp_content content = 108 List.map 109 (function 110 | Mcp_rpc.ToolsCall.ToolContent.Text t -> 111 Mcp.Text { TextContent.text = t.text; annotations = None } 112 | Mcp_rpc.ToolsCall.ToolContent.Image i -> 113 Mcp.Image 114 { 115 ImageContent.mime_type = i.mime_type; 116 data = i.data; 117 annotations = None; 118 } 119 | Mcp_rpc.ToolsCall.ToolContent.Audio a -> 120 Mcp.Audio 121 { 122 AudioContent.mime_type = a.mime_type; 123 data = a.data; 124 annotations = None; 125 } 126 | Mcp_rpc.ToolsCall.ToolContent.Resource r -> 127 (* Create a simple text resource from the embedded resource *) 128 let uri = 129 match r with 130 | { EmbeddedResource.resource = `Text tr; _ } -> tr.uri 131 | { EmbeddedResource.resource = `Blob br; _ } -> br.uri 132 in 133 let text_content = 134 match r with 135 | { EmbeddedResource.resource = `Text tr; _ } -> tr.text 136 | { EmbeddedResource.resource = `Blob br; _ } -> "Binary content" 137 in 138 let mime_type = 139 match r with 140 | { EmbeddedResource.resource = `Text tr; _ } -> tr.mime_type 141 | { EmbeddedResource.resource = `Blob br; _ } -> br.mime_type 142 in 143 let text_resource = 144 { TextResourceContents.uri; text = text_content; mime_type } 145 in 146 Mcp.Resource 147 { 148 EmbeddedResource.resource = `Text text_resource; 149 annotations = None; 150 }) 151 content 152 153 (* Convert Mcp.content list to Mcp_rpc.ToolsCall.ToolContent.t list *) 154 let mcp_content_to_rpc_content content = 155 List.map 156 (function 157 | Mcp.Text t -> Mcp_rpc.ToolsCall.ToolContent.Text t 158 | Mcp.Image img -> Mcp_rpc.ToolsCall.ToolContent.Image img 159 | Mcp.Audio aud -> Mcp_rpc.ToolsCall.ToolContent.Audio aud 160 | Mcp.Resource res -> 161 let resource_data = 162 match res.resource with 163 | `Text txt -> `Text txt 164 | `Blob blob -> `Blob blob 165 in 166 let resource = 167 { 168 EmbeddedResource.resource = resource_data; 169 annotations = res.annotations; 170 } 171 in 172 Mcp_rpc.ToolsCall.ToolContent.Resource resource) 173 content 174 175 (* Create a tool result with content *) 176 let create_tool_result content ~is_error = 177 `Assoc 178 [ 179 ("content", `List (List.map Mcp.yojson_of_content content)); 180 ("isError", `Bool is_error); 181 ] 182 183 (* Create a tool error result with structured content *) 184 let create_error_result error = 185 Log.errorf "Error result: %s" error; 186 create_tool_result [ Mcp.make_text_content error ] ~is_error:true 187 188 (* Handle tool execution errors *) 189 let handle_execution_error err = 190 create_error_result (Printf.sprintf "Error executing tool: %s" err) 191 192 (* Handle unknown tool error *) 193 let handle_unknown_tool_error name = 194 create_error_result (Printf.sprintf "Unknown tool: %s" name) 195 196 (* Handle general tool execution exception *) 197 let handle_execution_exception exn = 198 create_error_result 199 (Printf.sprintf "Internal error: %s" (Printexc.to_string exn)) 200end 201 202(* Resources for the MCP server *) 203module Resource = struct 204 type handler = Context.t -> string list -> (string, string) result 205 206 type t = { 207 uri : string; (* For resources, this is the exact URI (no variables) *) 208 name : string; 209 description : string option; 210 mime_type : string option; 211 handler : handler; 212 } 213 214 let create ~uri ~name ?description ?mime_type ~handler () = 215 (* Validate that the URI doesn't contain template variables *) 216 if String.contains uri '{' || String.contains uri '}' then 217 Log.warningf 218 "Resource '%s' contains template variables. Consider using \ 219 add_resource_template instead." 220 uri; 221 { uri; name; description; mime_type; handler } 222 223 let to_json resource = 224 let assoc = 225 [ ("uri", `String resource.uri); ("name", `String resource.name) ] 226 in 227 let assoc = 228 match resource.description with 229 | Some desc -> ("description", `String desc) :: assoc 230 | None -> assoc 231 in 232 let assoc = 233 match resource.mime_type with 234 | Some mime -> ("mimeType", `String mime) :: assoc 235 | None -> assoc 236 in 237 `Assoc assoc 238 239 (* Convert to Mcp_rpc.ResourcesList.Resource.t *) 240 let to_rpc_resource_list_resource (resource : t) = 241 Mcp_rpc.ResourcesList.Resource. 242 { 243 uri = resource.uri; 244 name = resource.name; 245 description = resource.description; 246 mime_type = resource.mime_type; 247 size = None; 248 (* Size can be added when we have actual resource content *) 249 } 250 251 (* Convert a list of Resource.t to the format needed for resources/list response *) 252 let to_rpc_resources_list resources = 253 List.map to_rpc_resource_list_resource resources 254end 255 256(* Prompts for the MCP server *) 257module Prompt = struct 258 type argument = { 259 name : string; 260 description : string option; 261 required : bool; 262 } 263 264 type message = { role : Role.t; content : content } 265 266 type handler = 267 Context.t -> (string * string) list -> (message list, string) result 268 269 type t = { 270 name : string; 271 description : string option; 272 arguments : argument list; 273 handler : handler; 274 } 275 276 let create ~name ?description ?(arguments = []) ~handler () = 277 { name; description; arguments; handler } 278 279 let create_argument ~name ?description ?(required = false) () = 280 { name; description; required } 281 282 let to_json prompt = 283 let assoc = [ ("name", `String prompt.name) ] in 284 let assoc = 285 match prompt.description with 286 | Some desc -> ("description", `String desc) :: assoc 287 | None -> assoc 288 in 289 let assoc = 290 if prompt.arguments <> [] then 291 let args = 292 List.map 293 (fun (arg : argument) -> 294 let arg_assoc = [ ("name", `String arg.name) ] in 295 let arg_assoc = 296 match arg.description with 297 | Some desc -> ("description", `String desc) :: arg_assoc 298 | None -> arg_assoc 299 in 300 let arg_assoc = 301 if arg.required then ("required", `Bool true) :: arg_assoc 302 else arg_assoc 303 in 304 `Assoc arg_assoc) 305 prompt.arguments 306 in 307 ("arguments", `List args) :: assoc 308 else assoc 309 in 310 `Assoc assoc 311 312 (* Convert argument to Mcp_rpc.PromptsList.PromptArgument.t *) 313 let argument_to_rpc_prompt_argument (arg : argument) = 314 Mcp_rpc.PromptsList.PromptArgument. 315 { 316 name = arg.name; 317 description = arg.description; 318 required = arg.required; 319 } 320 321 (* Convert to Mcp_rpc.PromptsList.Prompt.t *) 322 let to_rpc_prompt_list_prompt (prompt : t) = 323 Mcp_rpc.PromptsList.Prompt. 324 { 325 name = prompt.name; 326 description = prompt.description; 327 arguments = List.map argument_to_rpc_prompt_argument prompt.arguments; 328 } 329 330 (* Convert a list of Prompt.t to the format needed for prompts/list response *) 331 let to_rpc_prompts_list prompts = List.map to_rpc_prompt_list_prompt prompts 332 333 (* Convert message to Mcp_rpc.PromptMessage.t *) 334 let message_to_rpc_prompt_message msg = 335 { PromptMessage.role = msg.role; PromptMessage.content = msg.content } 336 337 (* Convert a list of messages to the format needed for prompts/get response *) 338 let messages_to_rpc_prompt_messages messages = 339 List.map message_to_rpc_prompt_message messages 340end 341 342let make_tool_schema properties required = 343 let props = 344 List.map 345 (fun (name, schema_type, description) -> 346 ( name, 347 `Assoc 348 [ 349 ("type", `String schema_type); ("description", `String description); 350 ] )) 351 properties 352 in 353 let required_json = `List (List.map (fun name -> `String name) required) in 354 `Assoc 355 [ 356 ("type", `String "object"); 357 ("properties", `Assoc props); 358 ("required", required_json); 359 ] 360 361(* Resource Templates for the MCP server *) 362module ResourceTemplate = struct 363 type handler = Context.t -> string list -> (string, string) result 364 365 type t = { 366 uri_template : string; 367 name : string; 368 description : string option; 369 mime_type : string option; 370 handler : handler; 371 } 372 373 let create ~uri_template ~name ?description ?mime_type ~handler () = 374 { uri_template; name; description; mime_type; handler } 375 376 let to_json resource_template = 377 let assoc = 378 [ 379 ("uriTemplate", `String resource_template.uri_template); 380 ("name", `String resource_template.name); 381 ] 382 in 383 let assoc = 384 match resource_template.description with 385 | Some desc -> ("description", `String desc) :: assoc 386 | None -> assoc 387 in 388 let assoc = 389 match resource_template.mime_type with 390 | Some mime -> ("mimeType", `String mime) :: assoc 391 | None -> assoc 392 in 393 `Assoc assoc 394 395 (* Convert to Mcp_rpc.ResourceTemplatesList.ResourceTemplate.t *) 396 let to_rpc_resource_template (template : t) = 397 Mcp_rpc.ListResourceTemplatesResult.ResourceTemplate. 398 { 399 uri_template = template.uri_template; 400 name = template.name; 401 description = template.description; 402 mime_type = template.mime_type; 403 } 404 405 (* Convert a list of ResourceTemplate.t to the format needed for resources/templates/list response *) 406 let to_rpc_resource_templates_list templates = 407 List.map to_rpc_resource_template templates 408end 409 410(* Main server type *) 411type server = { 412 name : string; 413 version : string; 414 protocol_version : string; 415 lifespan_context : (string * Json.t) list; 416 mutable capabilities : Json.t; 417 mutable tools : Tool.t list; 418 mutable resources : Resource.t list; 419 mutable resource_templates : ResourceTemplate.t list; 420 mutable prompts : Prompt.t list; 421} 422 423let name { name; _ } = name 424let version { version; _ } = version 425let capabilities { capabilities; _ } = capabilities 426let lifespan_context { lifespan_context; _ } = lifespan_context 427let protocol_version { protocol_version; _ } = protocol_version 428let tools { tools; _ } = tools 429let resources { resources; _ } = resources 430let resource_templates { resource_templates; _ } = resource_templates 431let prompts { prompts; _ } = prompts 432 433(* Create a new server *) 434let create_server ~name ?(version = "0.1.0") ?(protocol_version = "2024-11-05") 435 () = 436 { 437 name; 438 version; 439 protocol_version; 440 capabilities = `Assoc []; 441 tools = []; 442 resources = []; 443 resource_templates = []; 444 prompts = []; 445 lifespan_context = []; 446 } 447 448(* Default capabilities for the server *) 449let default_capabilities ?(with_tools = true) ?(with_resources = false) 450 ?(with_resource_templates = false) ?(with_prompts = false) () = 451 let caps = [] in 452 let caps = 453 if with_tools then ("tools", `Assoc [ ("listChanged", `Bool true) ]) :: caps 454 else caps 455 in 456 let caps = 457 if with_resources then 458 ( "resources", 459 `Assoc [ ("listChanged", `Bool true); ("subscribe", `Bool false) ] ) 460 :: caps 461 else if not with_resources then 462 ( "resources", 463 `Assoc [ ("listChanged", `Bool false); ("subscribe", `Bool false) ] ) 464 :: caps 465 else caps 466 in 467 let caps = 468 if with_resource_templates then 469 ("resourceTemplates", `Assoc [ ("listChanged", `Bool true) ]) :: caps 470 else if not with_resource_templates then 471 ("resourceTemplates", `Assoc [ ("listChanged", `Bool false) ]) :: caps 472 else caps 473 in 474 let caps = 475 if with_prompts then 476 ("prompts", `Assoc [ ("listChanged", `Bool true) ]) :: caps 477 else if not with_prompts then 478 ("prompts", `Assoc [ ("listChanged", `Bool false) ]) :: caps 479 else caps 480 in 481 `Assoc caps 482 483(* Register a tool *) 484let register_tool server tool = 485 server.tools <- tool :: server.tools; 486 tool 487 488(* Create and register a tool in one step *) 489let add_tool server ~name ?description ?(schema_properties = []) 490 ?(schema_required = []) handler = 491 let input_schema = make_tool_schema schema_properties schema_required in 492 let handler' ctx args = 493 try Ok (handler args) with exn -> Error (Printexc.to_string exn) 494 in 495 let tool = 496 Tool.create ~name ?description ~input_schema ~handler:handler' () 497 in 498 register_tool server tool 499 500(* Register a resource *) 501let register_resource server resource = 502 server.resources <- resource :: server.resources; 503 resource 504 505(* Create and register a resource in one step *) 506let add_resource server ~uri ~name ?description ?mime_type handler = 507 let handler' _ctx params = 508 try Ok (handler params) with exn -> Error (Printexc.to_string exn) 509 in 510 let resource = 511 Resource.create ~uri ~name ?description ?mime_type ~handler:handler' () 512 in 513 register_resource server resource 514 515(* Register a resource template *) 516let register_resource_template server template = 517 server.resource_templates <- template :: server.resource_templates; 518 template 519 520(* Create and register a resource template in one step *) 521let add_resource_template server ~uri_template ~name ?description ?mime_type 522 handler = 523 let handler' _ctx params = 524 try Ok (handler params) with exn -> Error (Printexc.to_string exn) 525 in 526 let template = 527 ResourceTemplate.create ~uri_template ~name ?description ?mime_type 528 ~handler:handler' () 529 in 530 register_resource_template server template 531 532(* Register a prompt *) 533let register_prompt server prompt = 534 server.prompts <- prompt :: server.prompts; 535 prompt 536 537(* Create and register a prompt in one step *) 538let add_prompt server ~name ?description ?(arguments = []) handler = 539 let prompt_args = 540 List.map 541 (fun (name, desc, required) -> 542 Prompt.create_argument ~name ?description:desc ~required ()) 543 arguments 544 in 545 let handler' _ctx args = 546 try Ok (handler args) with exn -> Error (Printexc.to_string exn) 547 in 548 let prompt = 549 Prompt.create ~name ?description ~arguments:prompt_args ~handler:handler' () 550 in 551 register_prompt server prompt 552 553(* Set server capabilities *) 554let set_capabilities server capabilities = server.capabilities <- capabilities 555 556(* Configure server with default capabilities based on registered components *) 557let configure_server server ?with_tools ?with_resources ?with_resource_templates 558 ?with_prompts () = 559 let with_tools = 560 match with_tools with Some b -> b | None -> server.tools <> [] 561 in 562 let with_resources = 563 match with_resources with Some b -> b | None -> server.resources <> [] 564 in 565 let with_resource_templates = 566 match with_resource_templates with 567 | Some b -> b 568 | None -> server.resource_templates <> [] 569 in 570 let with_prompts = 571 match with_prompts with Some b -> b | None -> server.prompts <> [] 572 in 573 let capabilities = 574 default_capabilities ~with_tools ~with_resources ~with_resource_templates 575 ~with_prompts () 576 in 577 set_capabilities server capabilities; 578 server