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 (fun oc -> Printf.fprintf oc "\n"; flush oc) stderr fmt 20 21 let debugf fmt = logf Debug fmt 22 let infof fmt = logf Info fmt 23 let warningf fmt = logf Warning fmt 24 let errorf fmt = logf Error fmt 25 26 (* Backward compatibility functions that take a simple string *) 27 let log level msg = logf level "%s" msg 28 let debug msg = debugf "%s" msg 29 let info msg = infof "%s" msg 30 let warning msg = warningf "%s" msg 31 let error msg = errorf "%s" msg 32end 33 34(* Context for tools and resources *) 35module Context = struct 36 type t = { 37 request_id: RequestId.t option; 38 lifespan_context: (string * Json.t) list; 39 progress_token: ProgressToken.t option; 40 } 41 42 let create ?request_id ?progress_token ?(lifespan_context=[]) () = 43 { request_id; lifespan_context; progress_token } 44 45 let get_context_value ctx key = 46 List.assoc_opt key ctx.lifespan_context 47 48 let report_progress ctx value total = 49 match ctx.progress_token, ctx.request_id with 50 | Some token, Some _id -> 51 let params = `Assoc [ 52 ("progress", `Float value); 53 ("total", `Float total); 54 ("progressToken", ProgressToken.yojson_of_t token) 55 ] in 56 Some (create_notification ~meth:Method.Progress ~params:(Some params) ()) 57 | _ -> None 58end 59 60(* Tools for the MCP server *) 61module Tool = struct 62 type handler = Context.t -> Json.t -> (Json.t, string) result 63 64 type t = { 65 name: string; 66 description: string option; 67 input_schema: Json.t; (* JSON Schema *) 68 handler: handler; 69 } 70 71 let create ~name ?description ~input_schema ~handler () = 72 { name; description; input_schema; handler } 73 74 let to_json tool = 75 let assoc = [ 76 ("name", `String tool.name); 77 ("inputSchema", tool.input_schema); 78 ] in 79 let assoc = match tool.description with 80 | Some desc -> ("description", `String desc) :: assoc 81 | None -> assoc 82 in 83 `Assoc assoc 84 85 (* Create a tool result with content *) 86 let create_tool_result content ~is_error = 87 `Assoc [ 88 ("content", `List (List.map Mcp.yojson_of_content content)); 89 ("isError", `Bool is_error); 90 ] 91 92 (* Create a tool error result with structured content *) 93 let create_error_result error = 94 Log.errorf "Error result: %s" error; 95 create_tool_result [Mcp.make_text_content error] ~is_error:true 96 97 (* Handle tool execution errors *) 98 let handle_execution_error err = 99 create_error_result (Printf.sprintf "Error executing tool: %s" err) 100 101 (* Handle unknown tool error *) 102 let handle_unknown_tool_error name = 103 create_error_result (Printf.sprintf "Unknown tool: %s" name) 104 105 (* Handle general tool execution exception *) 106 let handle_execution_exception exn = 107 create_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn)) 108end 109 110(* Resources for the MCP server *) 111module Resource = struct 112 type handler = Context.t -> string list -> (string, string) result 113 114 type t = { 115 uri_template: string; 116 description: string option; 117 mime_type: string option; 118 handler: handler; 119 } 120 121 let create ~uri_template ?description ?mime_type ~handler () = 122 { uri_template; description; mime_type; handler } 123 124 let to_json resource = 125 let assoc = [ 126 ("uriTemplate", `String resource.uri_template); 127 ] in 128 let assoc = match resource.description with 129 | Some desc -> ("description", `String desc) :: assoc 130 | None -> assoc 131 in 132 let assoc = match resource.mime_type with 133 | Some mime -> ("mimeType", `String mime) :: assoc 134 | None -> assoc 135 in 136 `Assoc assoc 137end 138 139(* Prompts for the MCP server *) 140module Prompt = struct 141 type argument = { 142 name: string; 143 description: string option; 144 required: bool; 145 } 146 147 type message = { 148 role: Role.t; 149 content: content; 150 } 151 152 type handler = Context.t -> (string * string) list -> (message list, string) result 153 154 type t = { 155 name: string; 156 description: string option; 157 arguments: argument list; 158 handler: handler; 159 } 160 161 let create ~name ?description ?(arguments=[]) ~handler () = 162 { name; description; arguments; handler } 163 164 let create_argument ~name ?description ?(required=false) () = 165 { name; description; required } 166 167 let to_json prompt = 168 let assoc = [ 169 ("name", `String prompt.name); 170 ] in 171 let assoc = match prompt.description with 172 | Some desc -> ("description", `String desc) :: assoc 173 | None -> assoc 174 in 175 let assoc = if prompt.arguments <> [] then 176 let args = List.map (fun (arg: argument) -> 177 let arg_assoc = [ 178 ("name", `String arg.name); 179 ] in 180 let arg_assoc = match arg.description with 181 | Some desc -> ("description", `String desc) :: arg_assoc 182 | None -> arg_assoc 183 in 184 let arg_assoc = 185 if arg.required then 186 ("required", `Bool true) :: arg_assoc 187 else 188 arg_assoc 189 in 190 `Assoc arg_assoc 191 ) prompt.arguments in 192 ("arguments", `List args) :: assoc 193 else 194 assoc 195 in 196 `Assoc assoc 197end 198 199let make_tool_schema properties required = 200 let props = List.map (fun (name, schema_type, description) -> 201 (name, `Assoc [ 202 ("type", `String schema_type); 203 ("description", `String description) 204 ]) 205 ) properties in 206 let required_json = `List (List.map (fun name -> `String name) required) in 207 `Assoc [ 208 ("type", `String "object"); 209 ("properties", `Assoc props); 210 ("required", required_json) 211 ] 212 213(* Main server type *) 214type server = { 215 name: string; 216 version: string; 217 protocol_version: string; 218 lifespan_context: (string * Json.t) list; 219 mutable capabilities: Json.t; 220 mutable tools: Tool.t list; 221 mutable resources: Resource.t list; 222 mutable prompts: Prompt.t list; 223} 224 225let name { name; _ } = name 226let version { version; _ } = version 227let capabilities { capabilities; _ } = capabilities 228let lifespan_context { lifespan_context; _ } = lifespan_context 229let protocol_version { protocol_version; _ } = protocol_version 230let tools { tools; _ } = tools 231let resources { resources; _ } = resources 232let prompts { prompts; _ } = prompts 233 234(* Create a new server *) 235let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () = 236 { 237 name; 238 version; 239 protocol_version; 240 capabilities = `Assoc []; 241 tools = []; 242 resources = []; 243 prompts = []; 244 lifespan_context = []; 245 } 246 247(* Default capabilities for the server *) 248let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () = 249 let caps = [] in 250 let caps = 251 if with_tools then 252 ("tools", `Assoc [ 253 ("listChanged", `Bool true) 254 ]) :: caps 255 else 256 caps 257 in 258 let caps = 259 if with_resources then 260 ("resources", `Assoc [ 261 ("listChanged", `Bool true); 262 ("subscribe", `Bool false) 263 ]) :: caps 264 else if not with_resources then 265 ("resources", `Assoc [ 266 ("listChanged", `Bool false); 267 ("subscribe", `Bool false) 268 ]) :: caps 269 else 270 caps 271 in 272 let caps = 273 if with_prompts then 274 ("prompts", `Assoc [ 275 ("listChanged", `Bool true) 276 ]) :: caps 277 else if not with_prompts then 278 ("prompts", `Assoc [ 279 ("listChanged", `Bool false) 280 ]) :: caps 281 else 282 caps 283 in 284 `Assoc caps 285 286(* Register a tool *) 287let register_tool server tool = 288 server.tools <- tool :: server.tools; 289 tool 290 291(* Create and register a tool in one step *) 292let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler = 293 let input_schema = make_tool_schema schema_properties schema_required in 294 let handler' ctx args = 295 try 296 Ok (handler args) 297 with exn -> 298 Error (Printexc.to_string exn) 299 in 300 let tool = Tool.create 301 ~name 302 ?description 303 ~input_schema 304 ~handler:handler' 305 () 306 in 307 register_tool server tool 308 309(* Register a resource *) 310let register_resource server resource = 311 server.resources <- resource :: server.resources; 312 resource 313 314(* Create and register a resource in one step *) 315let add_resource server ~uri_template ?description ?mime_type handler = 316 let handler' _ctx params = 317 try 318 Ok (handler params) 319 with exn -> 320 Error (Printexc.to_string exn) 321 in 322 let resource = Resource.create 323 ~uri_template 324 ?description 325 ?mime_type 326 ~handler:handler' 327 () 328 in 329 register_resource server resource 330 331(* Register a prompt *) 332let register_prompt server prompt = 333 server.prompts <- prompt :: server.prompts; 334 prompt 335 336(* Create and register a prompt in one step *) 337let add_prompt server ~name ?description ?(arguments=[]) handler = 338 let prompt_args = List.map (fun (name, desc, required) -> 339 Prompt.create_argument ~name ?description:desc ~required () 340 ) arguments in 341 let handler' _ctx args = 342 try 343 Ok (handler args) 344 with exn -> 345 Error (Printexc.to_string exn) 346 in 347 let prompt = Prompt.create 348 ~name 349 ?description 350 ~arguments:prompt_args 351 ~handler:handler' 352 () 353 in 354 register_prompt server prompt 355 356(* Set server capabilities *) 357let set_capabilities server capabilities = 358 server.capabilities <- capabilities 359 360(* Configure server with default capabilities based on registered components *) 361let configure_server server ?with_tools ?with_resources ?with_prompts () = 362 let with_tools = match with_tools with 363 | Some b -> b 364 | None -> server.tools <> [] 365 in 366 let with_resources = match with_resources with 367 | Some b -> b 368 | None -> server.resources <> [] 369 in 370 let with_prompts = match with_prompts with 371 | Some b -> b 372 | None -> server.prompts <> [] 373 in 374 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in 375 set_capabilities server capabilities; 376 server