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