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 (* Convert to Mcp_rpc.ToolsList.Tool.t *)
86 let to_rpc_tool_list_tool (tool:t) =
87 Mcp_rpc.ToolsList.Tool.{
88 name = tool.name;
89 description = tool.description;
90 input_schema = tool.input_schema;
91 annotations = None; (* Could be extended to support annotations *)
92 }
93
94 (* Convert a list of Tool.t to the format needed for tools/list response *)
95 let to_rpc_tools_list tools =
96 List.map to_rpc_tool_list_tool tools
97
98 (* Convert Mcp_rpc.ToolsCall response content to Mcp.content list *)
99 let rpc_content_to_mcp_content content =
100 List.map (function
101 | Mcp_rpc.ToolsCall.ToolContent.Text t ->
102 Mcp.Text { TextContent.text = t.text; annotations = None }
103 | Mcp_rpc.ToolsCall.ToolContent.Image i ->
104 Mcp.Image {
105 ImageContent.mime_type = i.mime_type;
106 data = i.data;
107 annotations = None
108 }
109 | Mcp_rpc.ToolsCall.ToolContent.Audio a ->
110 Mcp.Audio {
111 AudioContent.mime_type = a.mime_type;
112 data = a.data;
113 annotations = None
114 }
115 | Mcp_rpc.ToolsCall.ToolContent.Resource r ->
116 (* Create a simple text resource from the embedded resource *)
117 let uri = match r with
118 | { EmbeddedResource.resource = `Text tr; _ } -> tr.uri
119 | { EmbeddedResource.resource = `Blob br; _ } -> br.uri
120 in
121 let text_content = match r with
122 | { EmbeddedResource.resource = `Text tr; _ } -> tr.text
123 | { EmbeddedResource.resource = `Blob br; _ } -> "Binary content"
124 in
125 let mime_type = match r with
126 | { EmbeddedResource.resource = `Text tr; _ } -> tr.mime_type
127 | { EmbeddedResource.resource = `Blob br; _ } -> br.mime_type
128 in
129 let text_resource = {
130 TextResourceContents.uri;
131 text = text_content;
132 mime_type
133 } in
134 Mcp.Resource {
135 EmbeddedResource.resource = `Text text_resource;
136 annotations = None
137 }
138 ) content
139
140 (* Convert Mcp.content list to Mcp_rpc.ToolsCall.ToolContent.t list *)
141 let mcp_content_to_rpc_content content =
142 List.map (function
143 | Mcp.Text t ->
144 Mcp_rpc.ToolsCall.ToolContent.Text t
145 | Mcp.Image img ->
146 Mcp_rpc.ToolsCall.ToolContent.Image img
147 | Mcp.Audio aud ->
148 Mcp_rpc.ToolsCall.ToolContent.Audio aud
149 | Mcp.Resource res ->
150 let resource_data = match res.resource with
151 | `Text txt -> `Text txt
152 | `Blob blob -> `Blob blob
153 in
154 let resource = {
155 EmbeddedResource.resource = resource_data;
156 annotations = res.annotations
157 } in
158 Mcp_rpc.ToolsCall.ToolContent.Resource resource
159 ) content
160
161 (* Create a tool result with content *)
162 let create_tool_result content ~is_error =
163 `Assoc [
164 ("content", `List (List.map Mcp.yojson_of_content content));
165 ("isError", `Bool is_error);
166 ]
167
168 (* Create a tool error result with structured content *)
169 let create_error_result error =
170 Log.errorf "Error result: %s" error;
171 create_tool_result [Mcp.make_text_content error] ~is_error:true
172
173 (* Handle tool execution errors *)
174 let handle_execution_error err =
175 create_error_result (Printf.sprintf "Error executing tool: %s" err)
176
177 (* Handle unknown tool error *)
178 let handle_unknown_tool_error name =
179 create_error_result (Printf.sprintf "Unknown tool: %s" name)
180
181 (* Handle general tool execution exception *)
182 let handle_execution_exception exn =
183 create_error_result (Printf.sprintf "Internal error: %s" (Printexc.to_string exn))
184end
185
186(* Resources for the MCP server *)
187module Resource = struct
188 type handler = Context.t -> string list -> (string, string) result
189
190 type t = {
191 uri_template: string;
192 description: string option;
193 mime_type: string option;
194 handler: handler;
195 }
196
197 let create ~uri_template ?description ?mime_type ~handler () =
198 { uri_template; description; mime_type; handler }
199
200 let to_json resource =
201 let assoc = [
202 ("uriTemplate", `String resource.uri_template);
203 ] in
204 let assoc = match resource.description with
205 | Some desc -> ("description", `String desc) :: assoc
206 | None -> assoc
207 in
208 let assoc = match resource.mime_type with
209 | Some mime -> ("mimeType", `String mime) :: assoc
210 | None -> assoc
211 in
212 `Assoc assoc
213
214 (* Convert to Mcp_rpc.ResourcesList.Resource.t *)
215 let to_rpc_resource_list_resource (resource:t) =
216 Mcp_rpc.ResourcesList.Resource.{
217 uri = resource.uri_template;
218 name = resource.uri_template; (* Use uri as name by default *)
219 description = resource.description;
220 mime_type = resource.mime_type;
221 size = None; (* Size can be added when we have actual resource content *)
222 }
223
224 (* Convert a list of Resource.t to the format needed for resources/list response *)
225 let to_rpc_resources_list resources =
226 List.map to_rpc_resource_list_resource resources
227end
228
229(* Prompts for the MCP server *)
230module Prompt = struct
231 type argument = {
232 name: string;
233 description: string option;
234 required: bool;
235 }
236
237 type message = {
238 role: Role.t;
239 content: content;
240 }
241
242 type handler = Context.t -> (string * string) list -> (message list, string) result
243
244 type t = {
245 name: string;
246 description: string option;
247 arguments: argument list;
248 handler: handler;
249 }
250
251 let create ~name ?description ?(arguments=[]) ~handler () =
252 { name; description; arguments; handler }
253
254 let create_argument ~name ?description ?(required=false) () =
255 { name; description; required }
256
257 let to_json prompt =
258 let assoc = [
259 ("name", `String prompt.name);
260 ] in
261 let assoc = match prompt.description with
262 | Some desc -> ("description", `String desc) :: assoc
263 | None -> assoc
264 in
265 let assoc = if prompt.arguments <> [] then
266 let args = List.map (fun (arg: argument) ->
267 let arg_assoc = [
268 ("name", `String arg.name);
269 ] in
270 let arg_assoc = match arg.description with
271 | Some desc -> ("description", `String desc) :: arg_assoc
272 | None -> arg_assoc
273 in
274 let arg_assoc =
275 if arg.required then
276 ("required", `Bool true) :: arg_assoc
277 else
278 arg_assoc
279 in
280 `Assoc arg_assoc
281 ) prompt.arguments in
282 ("arguments", `List args) :: assoc
283 else
284 assoc
285 in
286 `Assoc assoc
287
288 (* Convert argument to Mcp_rpc.PromptsList.PromptArgument.t *)
289 let argument_to_rpc_prompt_argument (arg:argument) =
290 Mcp_rpc.PromptsList.PromptArgument.{
291 name = arg.name;
292 description = arg.description;
293 required = arg.required;
294 }
295
296 (* Convert to Mcp_rpc.PromptsList.Prompt.t *)
297 let to_rpc_prompt_list_prompt (prompt:t) =
298 Mcp_rpc.PromptsList.Prompt.{
299 name = prompt.name;
300 description = prompt.description;
301 arguments = List.map argument_to_rpc_prompt_argument prompt.arguments;
302 }
303
304 (* Convert a list of Prompt.t to the format needed for prompts/list response *)
305 let to_rpc_prompts_list prompts =
306 List.map to_rpc_prompt_list_prompt prompts
307
308 (* Convert message to Mcp_rpc.PromptMessage.t *)
309 let message_to_rpc_prompt_message msg =
310 {
311 PromptMessage.role = msg.role;
312 PromptMessage.content = msg.content;
313 }
314
315 (* Convert a list of messages to the format needed for prompts/get response *)
316 let messages_to_rpc_prompt_messages messages =
317 List.map message_to_rpc_prompt_message messages
318end
319
320let make_tool_schema properties required =
321 let props = List.map (fun (name, schema_type, description) ->
322 (name, `Assoc [
323 ("type", `String schema_type);
324 ("description", `String description)
325 ])
326 ) properties in
327 let required_json = `List (List.map (fun name -> `String name) required) in
328 `Assoc [
329 ("type", `String "object");
330 ("properties", `Assoc props);
331 ("required", required_json)
332 ]
333
334(* Main server type *)
335type server = {
336 name: string;
337 version: string;
338 protocol_version: string;
339 lifespan_context: (string * Json.t) list;
340 mutable capabilities: Json.t;
341 mutable tools: Tool.t list;
342 mutable resources: Resource.t list;
343 mutable prompts: Prompt.t list;
344}
345
346let name { name; _ } = name
347let version { version; _ } = version
348let capabilities { capabilities; _ } = capabilities
349let lifespan_context { lifespan_context; _ } = lifespan_context
350let protocol_version { protocol_version; _ } = protocol_version
351let tools { tools; _ } = tools
352let resources { resources; _ } = resources
353let prompts { prompts; _ } = prompts
354
355(* Create a new server *)
356let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () =
357 {
358 name;
359 version;
360 protocol_version;
361 capabilities = `Assoc [];
362 tools = [];
363 resources = [];
364 prompts = [];
365 lifespan_context = [];
366 }
367
368(* Default capabilities for the server *)
369let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
370 let caps = [] in
371 let caps =
372 if with_tools then
373 ("tools", `Assoc [
374 ("listChanged", `Bool true)
375 ]) :: caps
376 else
377 caps
378 in
379 let caps =
380 if with_resources then
381 ("resources", `Assoc [
382 ("listChanged", `Bool true);
383 ("subscribe", `Bool false)
384 ]) :: caps
385 else if not with_resources then
386 ("resources", `Assoc [
387 ("listChanged", `Bool false);
388 ("subscribe", `Bool false)
389 ]) :: caps
390 else
391 caps
392 in
393 let caps =
394 if with_prompts then
395 ("prompts", `Assoc [
396 ("listChanged", `Bool true)
397 ]) :: caps
398 else if not with_prompts then
399 ("prompts", `Assoc [
400 ("listChanged", `Bool false)
401 ]) :: caps
402 else
403 caps
404 in
405 `Assoc caps
406
407(* Register a tool *)
408let register_tool server tool =
409 server.tools <- tool :: server.tools;
410 tool
411
412(* Create and register a tool in one step *)
413let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler =
414 let input_schema = make_tool_schema schema_properties schema_required in
415 let handler' ctx args =
416 try
417 Ok (handler args)
418 with exn ->
419 Error (Printexc.to_string exn)
420 in
421 let tool = Tool.create
422 ~name
423 ?description
424 ~input_schema
425 ~handler:handler'
426 ()
427 in
428 register_tool server tool
429
430(* Register a resource *)
431let register_resource server resource =
432 server.resources <- resource :: server.resources;
433 resource
434
435(* Create and register a resource in one step *)
436let add_resource server ~uri_template ?description ?mime_type handler =
437 let handler' _ctx params =
438 try
439 Ok (handler params)
440 with exn ->
441 Error (Printexc.to_string exn)
442 in
443 let resource = Resource.create
444 ~uri_template
445 ?description
446 ?mime_type
447 ~handler:handler'
448 ()
449 in
450 register_resource server resource
451
452(* Register a prompt *)
453let register_prompt server prompt =
454 server.prompts <- prompt :: server.prompts;
455 prompt
456
457(* Create and register a prompt in one step *)
458let add_prompt server ~name ?description ?(arguments=[]) handler =
459 let prompt_args = List.map (fun (name, desc, required) ->
460 Prompt.create_argument ~name ?description:desc ~required ()
461 ) arguments in
462 let handler' _ctx args =
463 try
464 Ok (handler args)
465 with exn ->
466 Error (Printexc.to_string exn)
467 in
468 let prompt = Prompt.create
469 ~name
470 ?description
471 ~arguments:prompt_args
472 ~handler:handler'
473 ()
474 in
475 register_prompt server prompt
476
477(* Set server capabilities *)
478let set_capabilities server capabilities =
479 server.capabilities <- capabilities
480
481(* Configure server with default capabilities based on registered components *)
482let configure_server server ?with_tools ?with_resources ?with_prompts () =
483 let with_tools = match with_tools with
484 | Some b -> b
485 | None -> server.tools <> []
486 in
487 let with_resources = match with_resources with
488 | Some b -> b
489 | None -> server.resources <> []
490 in
491 let with_prompts = match with_prompts with
492 | Some b -> b
493 | None -> server.prompts <> []
494 in
495 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in
496 set_capabilities server capabilities;
497 server