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 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 to_json prompt =
137 let assoc = [
138 ("name", `String prompt.name);
139 ] in
140 let assoc = match prompt.description with
141 | Some desc -> ("description", `String desc) :: assoc
142 | None -> assoc
143 in
144 let assoc = if prompt.arguments <> [] then
145 let args = List.map (fun (arg: argument) ->
146 let arg_assoc = [
147 ("name", `String arg.name);
148 ] in
149 let arg_assoc = match arg.description with
150 | Some desc -> ("description", `String desc) :: arg_assoc
151 | None -> arg_assoc
152 in
153 let arg_assoc =
154 if arg.required then
155 ("required", `Bool true) :: arg_assoc
156 else
157 arg_assoc
158 in
159 `Assoc arg_assoc
160 ) prompt.arguments in
161 ("arguments", `List args) :: assoc
162 else
163 assoc
164 in
165 `Assoc assoc
166end
167
168(* Helper functions for creating common objects *)
169let make_text_content text =
170 Text (TextContent.{ text; annotations = None })
171
172let make_tool_schema properties required =
173 let props = List.map (fun (name, schema_type, description) ->
174 (name, `Assoc [
175 ("type", `String schema_type);
176 ("description", `String description)
177 ])
178 ) properties in
179 let required_json = `List (List.map (fun name -> `String name) required) in
180 `Assoc [
181 ("type", `String "object");
182 ("properties", `Assoc props);
183 ("required", required_json)
184 ]
185
186(* Main server type *)
187type server = {
188 name: string;
189 version: string;
190 protocol_version: string;
191 lifespan_context: (string * Json.t) list;
192 mutable capabilities: Json.t;
193 mutable tools: Tool.t list;
194 mutable resources: Resource.t list;
195 mutable prompts: Prompt.t list;
196}
197
198let name { name; _ } = name
199let version { version; _ } = version
200let capabilities { capabilities; _ } = capabilities
201let lifespan_context { lifespan_context; _ } = lifespan_context
202let protocol_version { protocol_version; _ } = protocol_version
203let tools { tools; _ } = tools
204let resources { resources; _ } = resources
205let prompts { prompts; _ } = prompts
206
207(* Create a new server *)
208let create_server ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") () =
209 {
210 name;
211 version;
212 protocol_version;
213 capabilities = `Assoc [];
214 tools = [];
215 resources = [];
216 prompts = [];
217 lifespan_context = [];
218 }
219
220(* Default capabilities for the server *)
221let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
222 let caps = [] in
223 let caps =
224 if with_tools then
225 ("tools", `Assoc [
226 ("listChanged", `Bool true)
227 ]) :: caps
228 else
229 caps
230 in
231 let caps =
232 if with_resources then
233 ("resources", `Assoc [
234 ("listChanged", `Bool true);
235 ("subscribe", `Bool false)
236 ]) :: caps
237 else if not with_resources then
238 ("resources", `Assoc [
239 ("listChanged", `Bool false);
240 ("subscribe", `Bool false)
241 ]) :: caps
242 else
243 caps
244 in
245 let caps =
246 if with_prompts then
247 ("prompts", `Assoc [
248 ("listChanged", `Bool true)
249 ]) :: caps
250 else if not with_prompts then
251 ("prompts", `Assoc [
252 ("listChanged", `Bool false)
253 ]) :: caps
254 else
255 caps
256 in
257 `Assoc caps
258
259(* Register a tool *)
260let register_tool server tool =
261 server.tools <- tool :: server.tools;
262 tool
263
264(* Create and register a tool in one step *)
265let add_tool server ~name ?description ?(schema_properties=[]) ?(schema_required=[]) handler =
266 let input_schema = make_tool_schema schema_properties schema_required in
267 let handler' ctx args =
268 try
269 Ok (handler args)
270 with exn ->
271 Error (Printexc.to_string exn)
272 in
273 let tool = Tool.create
274 ~name
275 ?description
276 ~input_schema
277 ~handler:handler'
278 ()
279 in
280 register_tool server tool
281
282(* Register a resource *)
283let register_resource server resource =
284 server.resources <- resource :: server.resources;
285 resource
286
287(* Create and register a resource in one step *)
288let add_resource server ~uri_template ?description ?mime_type handler =
289 let handler' _ctx params =
290 try
291 Ok (handler params)
292 with exn ->
293 Error (Printexc.to_string exn)
294 in
295 let resource = Resource.create
296 ~uri_template
297 ?description
298 ?mime_type
299 ~handler:handler'
300 ()
301 in
302 register_resource server resource
303
304(* Register a prompt *)
305let register_prompt server prompt =
306 server.prompts <- prompt :: server.prompts;
307 prompt
308
309(* Create and register a prompt in one step *)
310let add_prompt server ~name ?description ?(arguments=[]) handler =
311 let prompt_args = List.map (fun (name, desc, required) ->
312 Prompt.create_argument ~name ?description:desc ~required ()
313 ) arguments in
314 let handler' _ctx args =
315 try
316 Ok (handler args)
317 with exn ->
318 Error (Printexc.to_string exn)
319 in
320 let prompt = Prompt.create
321 ~name
322 ?description
323 ~arguments:prompt_args
324 ~handler:handler'
325 ()
326 in
327 register_prompt server prompt
328
329(* Set server capabilities *)
330let set_capabilities server capabilities =
331 server.capabilities <- capabilities
332
333(* Configure server with default capabilities based on registered components *)
334let configure_server server ?with_tools ?with_resources ?with_prompts () =
335 let with_tools = match with_tools with
336 | Some b -> b
337 | None -> server.tools <> []
338 in
339 let with_resources = match with_resources with
340 | Some b -> b
341 | None -> server.resources <> []
342 in
343 let with_prompts = match with_prompts with
344 | Some b -> b
345 | None -> server.prompts <> []
346 in
347 let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in
348 set_capabilities server capabilities;
349 server