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 progress_token: ProgressToken.t option; 33 } 34 35 let create ?request_id ?progress_token ?(lifespan_context=[]) () = 36 { request_id; lifespan_context; progress_token } 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_:Method.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(* Content type constructors have been moved to the Mcp module *) 169 170(* Tool result handling using Mcp_message.ToolsCall.ToolContent *) 171 172let create_tool_result contents ~is_error = 173 (* Use the original Mcp.content values as is *) 174 let content = List.map (fun content -> 175 match content with 176 | Text text_content -> 177 Mcp_message.ToolsCall.ToolContent.Text text_content 178 | Image image_content -> 179 Mcp_message.ToolsCall.ToolContent.Image image_content 180 | Audio audio_content -> 181 Mcp_message.ToolsCall.ToolContent.Audio audio_content 182 | Resource resource_content -> 183 Mcp_message.ToolsCall.ToolContent.Resource resource_content 184 ) contents in 185 186 (* Use the ToolsCall.Response module's JSON conversion *) 187 Mcp_message.ToolsCall.Response.yojson_of_t { content; is_error } 188 189(* Using error codes from Mcp.ErrorCode module *) 190 191let make_tool_schema properties required = 192 let props = List.map (fun (name, schema_type, description) -> 193 (name, `Assoc [ 194 ("type", `String schema_type); 195 ("description", `String description) 196 ]) 197 ) properties in 198 let required_json = `List (List.map (fun name -> `String name) required) in 199 `Assoc [ 200 ("type", `String "object"); 201 ("properties", `Assoc props); 202 ("required", required_json) 203 ] 204 205(* Main server type *) 206type server = { 207 name: string; 208 version: string; 209 protocol_version: string; 210 lifespan_context: (string * Json.t) list; 211 mutable capabilities: Json.t; 212 mutable tools: Tool.t list; 213 mutable resources: Resource.t list; 214 mutable prompts: Prompt.t list; 215} 216 217let name { name; _ } = name 218let version { version; _ } = version 219let capabilities { capabilities; _ } = capabilities 220let lifespan_context { lifespan_context; _ } = lifespan_context 221let protocol_version { protocol_version; _ } = protocol_version 222let tools { tools; _ } = tools 223let resources { resources; _ } = resources 224let prompts { prompts; _ } = prompts 225 226(* Create a new server *) 227let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () = 228 { 229 name; 230 version; 231 protocol_version; 232 capabilities = `Assoc []; 233 tools = []; 234 resources = []; 235 prompts = []; 236 lifespan_context = []; 237 } 238 239(* Default capabilities for the server *) 240let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () = 241 let caps = [] in 242 let caps = 243 if with_tools then 244 ("tools", `Assoc [ 245 ("listChanged", `Bool true) 246 ]) :: caps 247 else 248 caps 249 in 250 let caps = 251 if with_resources then 252 ("resources", `Assoc [ 253 ("listChanged", `Bool true); 254 ("subscribe", `Bool false) 255 ]) :: caps 256 else if not with_resources then 257 ("resources", `Assoc [ 258 ("listChanged", `Bool false); 259 ("subscribe", `Bool false) 260 ]) :: caps 261 else 262 caps 263 in 264 let caps = 265 if with_prompts then 266 ("prompts", `Assoc [ 267 ("listChanged", `Bool true) 268 ]) :: caps 269 else if not with_prompts then 270 ("prompts", `Assoc [ 271 ("listChanged", `Bool false) 272 ]) :: caps 273 else 274 caps 275 in 276 `Assoc caps 277 278(* Register a tool *) 279let register_tool server tool = 280 server.tools <- tool :: server.tools; 281 tool 282 283(* Create a rich tool result with multiple content types *) 284let create_rich_tool_result ?(text=None) ?(image=None) ?(audio=None) ?(resource=None) ~is_error () = 285 let contents = [] in 286 287 (* Add text content if provided *) 288 let contents = match text with 289 | Some text -> (Mcp.make_text_content text) :: contents 290 | None -> contents 291 in 292 293 (* Add image content if provided *) 294 let contents = match image with 295 | Some (data, mime_type) -> (Mcp.make_image_content data mime_type) :: contents 296 | None -> contents 297 in 298 299 (* Add audio content if provided *) 300 let contents = match audio with 301 | Some (data, mime_type) -> (Mcp.make_audio_content data mime_type) :: contents 302 | None -> contents 303 in 304 305 (* Add resource content if provided *) 306 let contents = match resource with 307 | Some (uri, data, is_blob, mime_type) -> 308 (if is_blob then 309 Mcp.make_resource_blob_content uri data mime_type 310 else 311 Mcp.make_resource_text_content uri data mime_type) :: contents 312 | None -> contents 313 in 314 315 (* Create the final tool result *) 316 create_tool_result (List.rev contents) ~is_error 317 318(* Create and register a tool in one step *) 319let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler = 320 let input_schema = make_tool_schema schema_properties schema_required in 321 let handler' ctx args = 322 try 323 Ok (handler args) 324 with exn -> 325 Error (Printexc.to_string exn) 326 in 327 let tool = Tool.create 328 ~name 329 ?description 330 ~input_schema 331 ~handler:handler' 332 () 333 in 334 register_tool server tool 335 336(* Register a resource *) 337let register_resource server resource = 338 server.resources <- resource :: server.resources; 339 resource 340 341(* Create and register a resource in one step *) 342let add_resource server ~uri_template ?description ?mime_type handler = 343 let handler' _ctx params = 344 try 345 Ok (handler params) 346 with exn -> 347 Error (Printexc.to_string exn) 348 in 349 let resource = Resource.create 350 ~uri_template 351 ?description 352 ?mime_type 353 ~handler:handler' 354 () 355 in 356 register_resource server resource 357 358(* Register a prompt *) 359let register_prompt server prompt = 360 server.prompts <- prompt :: server.prompts; 361 prompt 362 363(* Create and register a prompt in one step *) 364let add_prompt server ~name ?description ?(arguments=[]) handler = 365 let prompt_args = List.map (fun (name, desc, required) -> 366 Prompt.create_argument ~name ?description:desc ~required () 367 ) arguments in 368 let handler' _ctx args = 369 try 370 Ok (handler args) 371 with exn -> 372 Error (Printexc.to_string exn) 373 in 374 let prompt = Prompt.create 375 ~name 376 ?description 377 ~arguments:prompt_args 378 ~handler:handler' 379 () 380 in 381 register_prompt server prompt 382 383(* Set server capabilities *) 384let set_capabilities server capabilities = 385 server.capabilities <- capabilities 386 387(* Configure server with default capabilities based on registered components *) 388let configure_server server ?with_tools ?with_resources ?with_prompts () = 389 let with_tools = match with_tools with 390 | Some b -> b 391 | None -> server.tools <> [] 392 in 393 let with_resources = match with_resources with 394 | Some b -> b 395 | None -> server.resources <> [] 396 in 397 let with_prompts = match with_prompts with 398 | Some b -> b 399 | None -> server.prompts <> [] 400 in 401 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in 402 set_capabilities server capabilities; 403 server 404 405(* The MCP message helpers have been moved to Mcp_message module. 406 This module now reexports them through open statements. *)