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