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(* Server implementation *)
189module Server = struct
190 type startup_hook = unit -> unit
191 type shutdown_hook = unit -> unit
192
193 type t = {
194 name: string;
195 version: string;
196 protocol_version: string;
197 mutable capabilities: Json.t;
198 mutable tools: Tool.t list;
199 mutable resources: Resource.t list;
200 mutable prompts: Prompt.t list;
201 mutable lifespan_context: (string * Json.t) list;
202 startup_hook: startup_hook option;
203 shutdown_hook: shutdown_hook option;
204 }
205
206 let create ~name ?(version="0.1.0") ?(protocol_version="2024-11-05") ?startup_hook ?shutdown_hook () =
207 {
208 name;
209 version;
210 protocol_version;
211 capabilities = `Assoc [];
212 tools = [];
213 resources = [];
214 prompts = [];
215 lifespan_context = [];
216 startup_hook;
217 shutdown_hook;
218 }
219
220 (* Register a tool *)
221 let register_tool server tool =
222 server.tools <- tool :: server.tools;
223 ()
224
225 (* Register a resource *)
226 let register_resource server resource =
227 server.resources <- resource :: server.resources;
228 ()
229
230 (* Register a prompt *)
231 let register_prompt server prompt =
232 server.prompts <- prompt :: server.prompts;
233 ()
234
235 (* Default server capabilities *)
236 let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
237 let caps = [] in
238 let caps =
239 if with_tools then
240 ("tools", `Assoc [
241 ("listChanged", `Bool true)
242 ]) :: caps
243 else
244 caps
245 in
246 let caps =
247 if with_resources then
248 ("resources", `Assoc [
249 ("listChanged", `Bool true);
250 ("subscribe", `Bool false)
251 ]) :: caps
252 else if not with_resources then
253 ("resources", `Assoc [
254 ("listChanged", `Bool false);
255 ("subscribe", `Bool false)
256 ]) :: caps
257 else
258 caps
259 in
260 let caps =
261 if with_prompts then
262 ("prompts", `Assoc [
263 ("listChanged", `Bool true)
264 ]) :: caps
265 else if not with_prompts then
266 ("prompts", `Assoc [
267 ("listChanged", `Bool false)
268 ]) :: caps
269 else
270 caps
271 in
272 `Assoc caps
273
274 (* Update server capabilities *)
275 let update_capabilities server capabilities =
276 server.capabilities <- capabilities
277
278 (* Process a message *)
279 let process_message _server _json =
280 None
281
282 (* Main server loop *)
283 let run _server =
284 (* Placeholder implementation *)
285 ()
286end
287
288(* Helper function for default capabilities *)
289let default_capabilities = Server.default_capabilities
290
291(* Add syntactic sugar for creating a server *)
292module MakeServer(S: sig val name: string val version: string option end) = struct
293 let _config = (S.name, S.version) (* Used to prevent unused parameter warning *)
294
295 (* Create server *)
296 let server = Server.create
297 ~name:S.name
298 ?version:S.version
299 ~protocol_version:"2024-11-05"
300 ()
301
302 (* Create a tool *)
303 let tool ?name ?description ?(schema_properties=[]) ?(schema_required=[]) handler =
304 let name = match name with
305 | Some (Some n) -> n
306 | Some None | None -> "tool" in
307 let input_schema = make_tool_schema schema_properties schema_required in
308 let handler' ctx args =
309 try
310 Ok (handler args)
311 with exn ->
312 Error (Printexc.to_string exn)
313 in
314 let tool = Tool.create
315 ~name
316 ?description
317 ~input_schema
318 ~handler:handler'
319 ()
320 in
321 server.tools <- tool :: server.tools;
322 tool
323
324 (* Create a resource *)
325 let resource ?uri_template ?description ?mime_type handler =
326 let uri_template = match uri_template with
327 | Some (Some uri) -> uri
328 | Some None | None -> "resource://" in
329 let handler' ctx params =
330 try
331 Ok (handler params)
332 with exn ->
333 Error (Printexc.to_string exn)
334 in
335 let resource = Resource.create
336 ~uri_template
337 ?description
338 ?mime_type
339 ~handler:handler'
340 ()
341 in
342 server.resources <- resource :: server.resources;
343 resource
344
345 (* Create a prompt *)
346 let prompt ?name ?description ?(arguments=[]) handler =
347 let name = match name with
348 | Some (Some n) -> n
349 | Some None | None -> "prompt" in
350 let prompt_args = List.map (fun (name, desc, required) ->
351 Prompt.create_argument ~name ?description:desc ~required ()
352 ) arguments in
353 let handler' ctx args =
354 try
355 Ok (handler args)
356 with exn ->
357 Error (Printexc.to_string exn)
358 in
359 let prompt = Prompt.create
360 ~name
361 ?description
362 ~arguments:prompt_args
363 ~handler:handler'
364 ()
365 in
366 server.prompts <- prompt :: server.prompts;
367 prompt
368
369 (* Run the server *)
370 let run ?with_tools ?with_resources ?with_prompts () =
371 let with_tools = match with_tools with
372 | Some b -> b
373 | None -> server.tools <> []
374 in
375 let with_resources = match with_resources with
376 | Some b -> b
377 | None -> server.resources <> []
378 in
379 let with_prompts = match with_prompts with
380 | Some b -> b
381 | None -> server.prompts <> []
382 in
383 let capabilities = Server.default_capabilities ~with_tools ~with_resources ~with_prompts () in
384 server.capabilities <- capabilities;
385 Log.info "Starting server...";
386 Log.info (Printf.sprintf "Server info: %s v%s" server.name
387 (match S.version with Some v -> v | None -> "unknown"));
388 Printexc.record_backtrace true;
389 set_binary_mode_out stdout false;
390 Log.info "This is just a placeholder server implementation."
391end