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