Model Context Protocol in OCaml
at tmp 12 kB view raw
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 mutable progress_token: ProgressToken.t option; 33 } 34 35 let create ?request_id ?(lifespan_context=[]) () = 36 { request_id; lifespan_context; progress_token = None } 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_:"notifications/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 yojson_of_message { role; content } = 137 `Assoc [ 138 ("role", Role.yojson_of_t role); 139 ("content", yojson_of_content content); 140 ] 141 142 (* This function must match the structure expected by the PromptMessage module in mcp.ml *) 143 let message_of_yojson json = 144 match json with 145 | `Assoc fields -> begin 146 let role = match List.assoc_opt "role" fields with 147 | Some json -> begin 148 Role.t_of_yojson json 149 end 150 | None -> begin 151 raise (Json.Of_json ("Missing role field", `Assoc fields)) 152 end 153 in 154 let content = match List.assoc_opt "content" fields with 155 | Some json -> begin 156 content_of_yojson json 157 end 158 | None -> begin 159 raise (Json.Of_json ("Missing content field", `Assoc fields)) 160 end 161 in 162 { role; content } 163 end 164 | j -> begin 165 raise (Json.Of_json ("Expected object for PromptMessage", j)) 166 end 167 168 let to_json prompt = 169 let assoc = [ 170 ("name", `String prompt.name); 171 ] in 172 let assoc = match prompt.description with 173 | Some desc -> ("description", `String desc) :: assoc 174 | None -> assoc 175 in 176 let assoc = if prompt.arguments <> [] then 177 let args = List.map (fun (arg: argument) -> 178 let arg_assoc = [ 179 ("name", `String arg.name); 180 ] in 181 let arg_assoc = match arg.description with 182 | Some desc -> ("description", `String desc) :: arg_assoc 183 | None -> arg_assoc 184 in 185 let arg_assoc = 186 if arg.required then 187 ("required", `Bool true) :: arg_assoc 188 else 189 arg_assoc 190 in 191 `Assoc arg_assoc 192 ) prompt.arguments in 193 ("arguments", `List args) :: assoc 194 else 195 assoc 196 in 197 `Assoc assoc 198end 199 200(* Helper functions for creating common objects *) 201let make_text_content text = 202 Text (TextContent.{ text; annotations = None }) 203 204let make_text_content_with_annotations text annotations = 205 Text (TextContent.{ text; annotations = Some annotations }) 206 207let make_image_content data mime_type = 208 Image (ImageContent.{ data; mime_type; annotations = None }) 209 210let make_image_content_with_annotations data mime_type annotations = 211 Image (ImageContent.{ data; mime_type; annotations = Some annotations }) 212 213let make_audio_content data mime_type = 214 Audio (AudioContent.{ data; mime_type; annotations = None }) 215 216let make_audio_content_with_annotations data mime_type annotations = 217 Audio (AudioContent.{ data; mime_type; annotations = Some annotations }) 218 219let make_text_resource_content uri text ?mime_type () = 220 Resource (EmbeddedResource.{ 221 resource = `Text TextResourceContents.{ uri; text; mime_type }; 222 annotations = None 223 }) 224 225let make_blob_resource_content uri blob ?mime_type () = 226 Resource (EmbeddedResource.{ 227 resource = `Blob BlobResourceContents.{ uri; blob; mime_type }; 228 annotations = None 229 }) 230 231let make_tool_schema properties required = 232 let props = List.map (fun (name, schema_type, description) -> 233 (name, `Assoc [ 234 ("type", `String schema_type); 235 ("description", `String description) 236 ]) 237 ) properties in 238 let required_json = `List (List.map (fun name -> `String name) required) in 239 `Assoc [ 240 ("type", `String "object"); 241 ("properties", `Assoc props); 242 ("required", required_json) 243 ] 244 245(* Main server type *) 246type server = { 247 name: string; 248 version: string; 249 protocol_version: string; 250 mutable capabilities: Json.t; 251 mutable tools: Tool.t list; 252 mutable resources: Resource.t list; 253 mutable prompts: Prompt.t list; 254 mutable lifespan_context: (string * Json.t) list; 255 mutable startup_hook: (unit -> unit) option; 256 mutable shutdown_hook: (unit -> unit) option; 257} 258 259(* Create a new server *) 260let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () = 261 { 262 name; 263 version; 264 protocol_version; 265 capabilities = `Assoc []; 266 tools = []; 267 resources = []; 268 prompts = []; 269 lifespan_context = []; 270 startup_hook = None; 271 shutdown_hook = None; 272 } 273 274(* Default capabilities for the server *) 275let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () = 276 let caps = [] in 277 let caps = 278 if with_tools then 279 ("tools", `Assoc [ 280 ("listChanged", `Bool true) 281 ]) :: caps 282 else 283 caps 284 in 285 let caps = 286 if with_resources then 287 ("resources", `Assoc [ 288 ("listChanged", `Bool true); 289 ("subscribe", `Bool false) 290 ]) :: caps 291 else if not with_resources then 292 ("resources", `Assoc [ 293 ("listChanged", `Bool false); 294 ("subscribe", `Bool false) 295 ]) :: caps 296 else 297 caps 298 in 299 let caps = 300 if with_prompts then 301 ("prompts", `Assoc [ 302 ("listChanged", `Bool true) 303 ]) :: caps 304 else if not with_prompts then 305 ("prompts", `Assoc [ 306 ("listChanged", `Bool false) 307 ]) :: caps 308 else 309 caps 310 in 311 `Assoc caps 312 313(* Register a tool *) 314let register_tool server tool = 315 server.tools <- tool :: server.tools; 316 tool 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(* Set startup and shutdown hooks *) 406let set_startup_hook server hook = 407 server.startup_hook <- Some hook 408 409let set_shutdown_hook server hook = 410 server.shutdown_hook <- Some hook 411 412(* Transport type for server *) 413type transport_type = 414 | Stdio (* Read/write to stdin/stdout *) 415 | Http (* HTTP server - to be implemented *) 416 417(* Run server with stdio transport *) 418let run_server server = 419 (* Setup *) 420 Printexc.record_backtrace true; 421 422 Log.info (Printf.sprintf "%s server starting" server.name); 423 Log.debug (Printf.sprintf "Protocol version: %s" server.protocol_version); 424 Log.debug (Printf.sprintf "Server info: %s v%s" server.name server.version); 425 426 (* Initialize capabilities if not already set *) 427 if server.capabilities = `Assoc [] then 428 ignore (configure_server server ()); 429 430 (* Run startup hook if provided *) 431 (match server.startup_hook with 432 | Some hook -> hook () 433 | None -> ()); 434 435 (* This function will be replaced by a full implementation in the mcp_server module *) 436 Log.info "Server initialized and ready." 437 438(* Placeholder for running server with different transports *) 439let run_server_with_transport server transport = 440 match transport with 441 | Http -> 442 Log.info "HTTP server not implemented in this version, using stdio instead"; 443 run_server server 444 | Stdio -> 445 run_server server