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 create_tool_result [Mcp.make_text_content error] ~is_error:true 95 96 (* Handle tool execution errors *) 97 let handle_execution_error err = 98 Log.errorf "Tool execution failed: %s" 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 Log.errorf "Unknown tool: %s" name; 104 create_error_result (Printf.sprintf "Unknown tool: %s" name) 105 106 (* Handle general tool execution exception *) 107 let handle_execution_exception exn = 108 Log.errorf "Exception executing tool: %s" (Printexc.to_string exn); 109 create_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn)) 110end 111 112(* Resources for the MCP server *) 113module Resource = struct 114 type handler = Context.t -> string list -> (string, string) result 115 116 type t = { 117 uri_template: string; 118 description: string option; 119 mime_type: string option; 120 handler: handler; 121 } 122 123 let create ~uri_template ?description ?mime_type ~handler () = 124 { uri_template; description; mime_type; handler } 125 126 let to_json resource = 127 let assoc = [ 128 ("uriTemplate", `String resource.uri_template); 129 ] in 130 let assoc = match resource.description with 131 | Some desc -> ("description", `String desc) :: assoc 132 | None -> assoc 133 in 134 let assoc = match resource.mime_type with 135 | Some mime -> ("mimeType", `String mime) :: assoc 136 | None -> assoc 137 in 138 `Assoc assoc 139end 140 141(* Prompts for the MCP server *) 142module Prompt = struct 143 type argument = { 144 name: string; 145 description: string option; 146 required: bool; 147 } 148 149 type message = { 150 role: Role.t; 151 content: content; 152 } 153 154 type handler = Context.t -> (string * string) list -> (message list, string) result 155 156 type t = { 157 name: string; 158 description: string option; 159 arguments: argument list; 160 handler: handler; 161 } 162 163 let create ~name ?description ?(arguments=[]) ~handler () = 164 { name; description; arguments; handler } 165 166 let create_argument ~name ?description ?(required=false) () = 167 { name; description; required } 168 169 let to_json prompt = 170 let assoc = [ 171 ("name", `String prompt.name); 172 ] in 173 let assoc = match prompt.description with 174 | Some desc -> ("description", `String desc) :: assoc 175 | None -> assoc 176 in 177 let assoc = if prompt.arguments <> [] then 178 let args = List.map (fun (arg: argument) -> 179 let arg_assoc = [ 180 ("name", `String arg.name); 181 ] in 182 let arg_assoc = match arg.description with 183 | Some desc -> ("description", `String desc) :: arg_assoc 184 | None -> arg_assoc 185 in 186 let arg_assoc = 187 if arg.required then 188 ("required", `Bool true) :: arg_assoc 189 else 190 arg_assoc 191 in 192 `Assoc arg_assoc 193 ) prompt.arguments in 194 ("arguments", `List args) :: assoc 195 else 196 assoc 197 in 198 `Assoc assoc 199end 200 201let make_tool_schema properties required = 202 let props = List.map (fun (name, schema_type, description) -> 203 (name, `Assoc [ 204 ("type", `String schema_type); 205 ("description", `String description) 206 ]) 207 ) properties in 208 let required_json = `List (List.map (fun name -> `String name) required) in 209 `Assoc [ 210 ("type", `String "object"); 211 ("properties", `Assoc props); 212 ("required", required_json) 213 ] 214 215(* Main server type *) 216type server = { 217 name: string; 218 version: string; 219 protocol_version: string; 220 lifespan_context: (string * Json.t) list; 221 mutable capabilities: Json.t; 222 mutable tools: Tool.t list; 223 mutable resources: Resource.t list; 224 mutable prompts: Prompt.t list; 225} 226 227let name { name; _ } = name 228let version { version; _ } = version 229let capabilities { capabilities; _ } = capabilities 230let lifespan_context { lifespan_context; _ } = lifespan_context 231let protocol_version { protocol_version; _ } = protocol_version 232let tools { tools; _ } = tools 233let resources { resources; _ } = resources 234let prompts { prompts; _ } = prompts 235 236(* Create a new server *) 237let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () = 238 { 239 name; 240 version; 241 protocol_version; 242 capabilities = `Assoc []; 243 tools = []; 244 resources = []; 245 prompts = []; 246 lifespan_context = []; 247 } 248 249(* Default capabilities for the server *) 250let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () = 251 let caps = [] in 252 let caps = 253 if with_tools then 254 ("tools", `Assoc [ 255 ("listChanged", `Bool true) 256 ]) :: caps 257 else 258 caps 259 in 260 let caps = 261 if with_resources then 262 ("resources", `Assoc [ 263 ("listChanged", `Bool true); 264 ("subscribe", `Bool false) 265 ]) :: caps 266 else if not with_resources then 267 ("resources", `Assoc [ 268 ("listChanged", `Bool false); 269 ("subscribe", `Bool false) 270 ]) :: caps 271 else 272 caps 273 in 274 let caps = 275 if with_prompts then 276 ("prompts", `Assoc [ 277 ("listChanged", `Bool true) 278 ]) :: caps 279 else if not with_prompts then 280 ("prompts", `Assoc [ 281 ("listChanged", `Bool false) 282 ]) :: caps 283 else 284 caps 285 in 286 `Assoc caps 287 288(* Register a tool *) 289let register_tool server tool = 290 server.tools <- tool :: server.tools; 291 tool 292 293(* Create and register a tool in one step *) 294let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler = 295 let input_schema = make_tool_schema schema_properties schema_required in 296 let handler' ctx args = 297 try 298 Ok (handler args) 299 with exn -> 300 Error (Printexc.to_string exn) 301 in 302 let tool = Tool.create 303 ~name 304 ?description 305 ~input_schema 306 ~handler:handler' 307 () 308 in 309 register_tool server tool 310 311(* Register a resource *) 312let register_resource server resource = 313 server.resources <- resource :: server.resources; 314 resource 315 316(* Create and register a resource in one step *) 317let add_resource server ~uri_template ?description ?mime_type handler = 318 let handler' _ctx params = 319 try 320 Ok (handler params) 321 with exn -> 322 Error (Printexc.to_string exn) 323 in 324 let resource = Resource.create 325 ~uri_template 326 ?description 327 ?mime_type 328 ~handler:handler' 329 () 330 in 331 register_resource server resource 332 333(* Register a prompt *) 334let register_prompt server prompt = 335 server.prompts <- prompt :: server.prompts; 336 prompt 337 338(* Create and register a prompt in one step *) 339let add_prompt server ~name ?description ?(arguments=[]) handler = 340 let prompt_args = List.map (fun (name, desc, required) -> 341 Prompt.create_argument ~name ?description:desc ~required () 342 ) arguments in 343 let handler' _ctx args = 344 try 345 Ok (handler args) 346 with exn -> 347 Error (Printexc.to_string exn) 348 in 349 let prompt = Prompt.create 350 ~name 351 ?description 352 ~arguments:prompt_args 353 ~handler:handler' 354 () 355 in 356 register_prompt server prompt 357 358(* Set server capabilities *) 359let set_capabilities server capabilities = 360 server.capabilities <- capabilities 361 362(* Configure server with default capabilities based on registered components *) 363let configure_server server ?with_tools ?with_resources ?with_prompts () = 364 let with_tools = match with_tools with 365 | Some b -> b 366 | None -> server.tools <> [] 367 in 368 let with_resources = match with_resources with 369 | Some b -> b 370 | None -> server.resources <> [] 371 in 372 let with_prompts = match with_prompts with 373 | Some b -> b 374 | None -> server.prompts <> [] 375 in 376 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in 377 set_capabilities server capabilities; 378 server