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 log level msg = 18 Printf.eprintf "[%s] %s\n" (string_of_level level) msg; 19 flush stderr 20 21 let debug = log Debug 22 let info = log Info 23 let warning = log Warning 24 let error = log Error 25end 26 27(* Context for tools and resources *) 28module Context = struct 29 type t = { 30 request_id: RequestId.t option; 31 lifespan_context: (string * Json.t) list; 32 mutable progress_token: ProgressToken.t option; 33 } 34 35 let create ?request_id ?(lifespan_context=[]) () = 36 { request_id; lifespan_context; progress_token = None } 37 38 let get_context_value ctx key = 39 List.assoc_opt key ctx.lifespan_context 40 41 let report_progress ctx value total = 42 match ctx.progress_token, ctx.request_id with 43 | Some token, Some _id -> 44 let params = `Assoc [ 45 ("progress", `Float value); 46 ("total", `Float total); 47 ("progressToken", ProgressToken.yojson_of_t token) 48 ] in 49 Some (create_notification ~method_:"notifications/progress" ~params:(Some params) ()) 50 | _ -> None 51end 52 53(* Tools for the MCP server *) 54module Tool = struct 55 type handler = Context.t -> Json.t -> (Json.t, string) result 56 57 type t = { 58 name: string; 59 description: string option; 60 input_schema: Json.t; (* JSON Schema *) 61 handler: handler; 62 } 63 64 let create ~name ?description ~input_schema ~handler () = 65 { name; description; input_schema; handler } 66 67 let to_json tool = 68 let assoc = [ 69 ("name", `String tool.name); 70 ("inputSchema", tool.input_schema); 71 ] in 72 let assoc = match tool.description with 73 | Some desc -> ("description", `String desc) :: assoc 74 | None -> assoc 75 in 76 `Assoc assoc 77end 78 79(* Resources for the MCP server *) 80module Resource = struct 81 type handler = Context.t -> string list -> (string, string) result 82 83 type t = { 84 uri_template: string; 85 description: string option; 86 mime_type: string option; 87 handler: handler; 88 } 89 90 let create ~uri_template ?description ?mime_type ~handler () = 91 { uri_template; description; mime_type; handler } 92 93 let to_json resource = 94 let assoc = [ 95 ("uriTemplate", `String resource.uri_template); 96 ] in 97 let assoc = match resource.description with 98 | Some desc -> ("description", `String desc) :: assoc 99 | None -> assoc 100 in 101 let assoc = match resource.mime_type with 102 | Some mime -> ("mimeType", `String mime) :: assoc 103 | None -> assoc 104 in 105 `Assoc assoc 106end 107 108(* Prompts for the MCP server *) 109module Prompt = struct 110 type argument = { 111 name: string; 112 description: string option; 113 required: bool; 114 } 115 116 type message = { 117 role: Role.t; 118 content: content; 119 } 120 121 type handler = Context.t -> (string * string) list -> (message list, string) result 122 123 type t = { 124 name: string; 125 description: string option; 126 arguments: argument list; 127 handler: handler; 128 } 129 130 let create ~name ?description ?(arguments=[]) ~handler () = 131 { name; description; arguments; handler } 132 133 let create_argument ~name ?description ?(required=false) () = 134 { name; description; required } 135 136 let to_json prompt = 137 let assoc = [ 138 ("name", `String prompt.name); 139 ] in 140 let assoc = match prompt.description with 141 | Some desc -> ("description", `String desc) :: assoc 142 | None -> assoc 143 in 144 let assoc = if prompt.arguments <> [] then 145 let args = List.map (fun (arg: argument) -> 146 let arg_assoc = [ 147 ("name", `String arg.name); 148 ] in 149 let arg_assoc = match arg.description with 150 | Some desc -> ("description", `String desc) :: arg_assoc 151 | None -> arg_assoc 152 in 153 let arg_assoc = 154 if arg.required then 155 ("required", `Bool true) :: arg_assoc 156 else 157 arg_assoc 158 in 159 `Assoc arg_assoc 160 ) prompt.arguments in 161 ("arguments", `List args) :: assoc 162 else 163 assoc 164 in 165 `Assoc assoc 166end 167 168(* Helper functions for creating common objects *) 169(* Content type constructors *) 170let make_text_content text = 171 Text (TextContent.{ text; annotations = None }) 172 173let make_image_content data mime_type = 174 Image (ImageContent.{ data; mime_type; annotations = None }) 175 176(* Create audio content using the AudioContent module *) 177let make_audio_content data mime_type = 178 Audio (AudioContent.{ data; mime_type; annotations = None }) 179 180let make_resource_text_content uri text mime_type = 181 Resource (EmbeddedResource.{ 182 resource = `Text TextResourceContents.{ uri; text; mime_type }; 183 annotations = None; 184 }) 185 186let make_resource_blob_content uri blob mime_type = 187 Resource (EmbeddedResource.{ 188 resource = `Blob BlobResourceContents.{ uri; blob; mime_type }; 189 annotations = None; 190 }) 191 192(* Tool result handling *) 193type tool_content = 194 | TextContent of string 195 | ImageContent of { data: string; mime_type: string } 196 | AudioContent of { data: string; mime_type: string } 197 | ResourceContent of { uri: string; data: string; is_blob: bool; mime_type: string option } 198 199let create_tool_result contents ~is_error = 200 let content_json = List.map (function 201 | TextContent text -> 202 `Assoc [ 203 ("type", `String "text"); 204 ("text", `String text) 205 ] 206 | ImageContent { data; mime_type } -> 207 `Assoc [ 208 ("type", `String "image"); 209 ("data", `String data); 210 ("mimeType", `String mime_type) 211 ] 212 | AudioContent { data; mime_type } -> 213 `Assoc [ 214 ("type", `String "audio"); 215 ("data", `String data); 216 ("mimeType", `String mime_type) 217 ] 218 | ResourceContent { uri; data; is_blob; mime_type } -> 219 let resource_data = if is_blob then 220 `Assoc ( 221 [("uri", `String uri); 222 ("blob", `String data)] @ 223 (match mime_type with 224 | Some mime -> [("mimeType", `String mime)] 225 | None -> []) 226 ) 227 else 228 `Assoc ( 229 [("uri", `String uri); 230 ("text", `String data)] @ 231 (match mime_type with 232 | Some mime -> [("mimeType", `String mime)] 233 | None -> []) 234 ) 235 in 236 `Assoc [ 237 ("type", `String "resource"); 238 ("resource", resource_data) 239 ] 240 ) contents in 241 242 `Assoc [ 243 ("content", `List content_json); 244 ("isError", `Bool is_error) 245 ] 246 247(* Error types with standard JSON-RPC error codes *) 248type error_code = 249 | ParseError (* -32700 *) 250 | InvalidRequest (* -32600 *) 251 | MethodNotFound (* -32601 *) 252 | InvalidParams (* -32602 *) 253 | InternalError (* -32603 *) 254 | ResourceNotFound (* -32002 Custom code for MCP *) 255 | AuthenticationRequired (* -32001 Custom code for MCP *) 256 | CustomError of int 257 258let error_code_to_int = function 259 | ParseError -> -32700 260 | InvalidRequest -> -32600 261 | MethodNotFound -> -32601 262 | InvalidParams -> -32602 263 | InternalError -> -32603 264 | ResourceNotFound -> -32002 265 | AuthenticationRequired -> -32001 266 | CustomError code -> code 267 268let make_tool_schema properties required = 269 let props = List.map (fun (name, schema_type, description) -> 270 (name, `Assoc [ 271 ("type", `String schema_type); 272 ("description", `String description) 273 ]) 274 ) properties in 275 let required_json = `List (List.map (fun name -> `String name) required) in 276 `Assoc [ 277 ("type", `String "object"); 278 ("properties", `Assoc props); 279 ("required", required_json) 280 ] 281 282(* Main server type *) 283type server = { 284 name: string; 285 version: string; 286 protocol_version: string; 287 lifespan_context: (string * Json.t) list; 288 mutable capabilities: Json.t; 289 mutable tools: Tool.t list; 290 mutable resources: Resource.t list; 291 mutable prompts: Prompt.t list; 292} 293 294let name { name; _ } = name 295let version { version; _ } = version 296let capabilities { capabilities; _ } = capabilities 297let lifespan_context { lifespan_context; _ } = lifespan_context 298let protocol_version { protocol_version; _ } = protocol_version 299let tools { tools; _ } = tools 300let resources { resources; _ } = resources 301let prompts { prompts; _ } = prompts 302 303(* Create a new server *) 304let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () = 305 { 306 name; 307 version; 308 protocol_version; 309 capabilities = `Assoc []; 310 tools = []; 311 resources = []; 312 prompts = []; 313 lifespan_context = []; 314 } 315 316(* Default capabilities for the server *) 317let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () = 318 let caps = [] in 319 let caps = 320 if with_tools then 321 ("tools", `Assoc [ 322 ("listChanged", `Bool true) 323 ]) :: caps 324 else 325 caps 326 in 327 let caps = 328 if with_resources then 329 ("resources", `Assoc [ 330 ("listChanged", `Bool true); 331 ("subscribe", `Bool false) 332 ]) :: caps 333 else if not with_resources then 334 ("resources", `Assoc [ 335 ("listChanged", `Bool false); 336 ("subscribe", `Bool false) 337 ]) :: caps 338 else 339 caps 340 in 341 let caps = 342 if with_prompts then 343 ("prompts", `Assoc [ 344 ("listChanged", `Bool true) 345 ]) :: caps 346 else if not with_prompts then 347 ("prompts", `Assoc [ 348 ("listChanged", `Bool false) 349 ]) :: caps 350 else 351 caps 352 in 353 `Assoc caps 354 355(* Register a tool *) 356let register_tool server tool = 357 server.tools <- tool :: server.tools; 358 tool 359 360(* Create a rich tool result with multiple content types *) 361let create_rich_tool_result ?(text=None) ?(image=None) ?(audio=None) ?(resource=None) ~is_error () = 362 let contents = [] in 363 364 (* Add text content if provided *) 365 let contents = match text with 366 | Some text -> (TextContent text) :: contents 367 | None -> contents 368 in 369 370 (* Add image content if provided *) 371 let contents = match image with 372 | Some (data, mime_type) -> (ImageContent { data; mime_type }) :: contents 373 | None -> contents 374 in 375 376 (* Add audio content if provided *) 377 let contents = match audio with 378 | Some (data, mime_type) -> (AudioContent { data; mime_type }) :: contents 379 | None -> contents 380 in 381 382 (* Add resource content if provided *) 383 let contents = match resource with 384 | Some (uri, data, is_blob, mime_type) -> 385 (ResourceContent { uri; data; is_blob; mime_type }) :: contents 386 | None -> contents 387 in 388 389 (* Create the final tool result *) 390 create_tool_result (List.rev contents) ~is_error 391 392(* Create and register a tool in one step *) 393let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler = 394 let input_schema = make_tool_schema schema_properties schema_required in 395 let handler' ctx args = 396 try 397 Ok (handler args) 398 with exn -> 399 Error (Printexc.to_string exn) 400 in 401 let tool = Tool.create 402 ~name 403 ?description 404 ~input_schema 405 ~handler:handler' 406 () 407 in 408 register_tool server tool 409 410(* Register a resource *) 411let register_resource server resource = 412 server.resources <- resource :: server.resources; 413 resource 414 415(* Create and register a resource in one step *) 416let add_resource server ~uri_template ?description ?mime_type handler = 417 let handler' _ctx params = 418 try 419 Ok (handler params) 420 with exn -> 421 Error (Printexc.to_string exn) 422 in 423 let resource = Resource.create 424 ~uri_template 425 ?description 426 ?mime_type 427 ~handler:handler' 428 () 429 in 430 register_resource server resource 431 432(* Register a prompt *) 433let register_prompt server prompt = 434 server.prompts <- prompt :: server.prompts; 435 prompt 436 437(* Create and register a prompt in one step *) 438let add_prompt server ~name ?description ?(arguments=[]) handler = 439 let prompt_args = List.map (fun (name, desc, required) -> 440 Prompt.create_argument ~name ?description:desc ~required () 441 ) arguments in 442 let handler' _ctx args = 443 try 444 Ok (handler args) 445 with exn -> 446 Error (Printexc.to_string exn) 447 in 448 let prompt = Prompt.create 449 ~name 450 ?description 451 ~arguments:prompt_args 452 ~handler:handler' 453 () 454 in 455 register_prompt server prompt 456 457(* Set server capabilities *) 458let set_capabilities server capabilities = 459 server.capabilities <- capabilities 460 461(* Configure server with default capabilities based on registered components *) 462let configure_server server ?with_tools ?with_resources ?with_prompts () = 463 let with_tools = match with_tools with 464 | Some b -> b 465 | None -> server.tools <> [] 466 in 467 let with_resources = match with_resources with 468 | Some b -> b 469 | None -> server.resources <> [] 470 in 471 let with_prompts = match with_prompts with 472 | Some b -> b 473 | None -> server.prompts <> [] 474 in 475 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in 476 set_capabilities server capabilities; 477 server