Model Context Protocol in OCaml

Implement resource template handling for URI parameters

- Add ResourceTemplate module with handlers for templated URIs
- Update Resource module to use direct URIs without templates
- Implement URI parameter extraction from template patterns
- Fix handling of resources/read to match against both resources and templates
- Refactor resource matching into a dedicated Resource_matcher module
- Update example code to use resource templates properly
- Fix JSON-RPC method names for consistency

+3 -2
bin/capitalize_sdk.ml
···
}
)
-
(* Define and register a resource example *)
-
let _ = add_resource server
+
(* Define and register a resource template example *)
+
let _ = add_resource_template server
~uri_template:"greeting://{name}"
+
~name:"Greeting"
~description:"Get a greeting for a name"
~mime_type:"text/plain"
(fun params ->
+3 -2
bin/multimodal_sdk.ml
···
Tool.create_tool_result [Mcp.make_text_content (Printf.sprintf "Error: %s" msg)] ~is_error:true
)
-
(* Define and register a resource example with multimodal content *)
-
let _ = add_resource server
+
(* Define and register a resource template example with multimodal content *)
+
let _ = add_resource_template server
~uri_template:"multimodal://{name}"
+
~name:"Multimodal Greeting"
~description:"Get a multimodal greeting with text, image and audio"
~mime_type:"application/json"
(fun params ->
+3 -3
lib/mcp.ml
···
(* Resource methods *)
| ResourcesList
| ResourcesRead
-
| ResourcesTemplatesList
+
| ResourceTemplatesList
| ResourcesSubscribe
| ResourcesListChanged
| ResourcesUpdated
···
| Initialized -> "notifications/initialized"
| ResourcesList -> "resources/list"
| ResourcesRead -> "resources/read"
-
| ResourcesTemplatesList -> "resources/templates/list"
+
| ResourceTemplatesList -> "resources/templates/list"
| ResourcesSubscribe -> "resources/subscribe"
| ResourcesListChanged -> "notifications/resources/list_changed"
| ResourcesUpdated -> "notifications/resources/updated"
···
| "notifications/initialized" -> Initialized
| "resources/list" -> ResourcesList
| "resources/read" -> ResourcesRead
-
| "resources/templates/list" -> ResourcesTemplatesList
+
| "resources/templates/list" -> ResourceTemplatesList
| "resources/subscribe" -> ResourcesSubscribe
| "notifications/resources/list_changed" -> ResourcesListChanged
| "notifications/resources/updated" -> ResourcesUpdated
+1 -1
lib/mcp.mli
···
(* Resource methods *)
| ResourcesList (** Discover available resources *)
| ResourcesRead (** Retrieve resource contents *)
-
| ResourcesTemplatesList (** List available resource templates *)
+
| ResourceTemplatesList (** List available resource templates *)
| ResourcesSubscribe (** Subscribe to resource changes *)
| ResourcesListChanged (** Resource list has changed *)
| ResourcesUpdated (** Resource has been updated *)
+108
lib/mcp_rpc.ml
···
JSONRPCMessage.create_response ~id ~result
end
+
(* Resources/Templates/List *)
+
module ListResourceTemplatesRequest = struct
+
type t = {
+
cursor: Cursor.t option;
+
}
+
+
let yojson_of_t { cursor } =
+
let assoc = [] in
+
let assoc = match cursor with
+
| Some c -> ("cursor", Cursor.yojson_of_t c) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields ->
+
let cursor = List.assoc_opt "cursor" fields |> Option.map Cursor.t_of_yojson in
+
{ cursor }
+
| j -> Util.json_error "Expected object for ListResourceTemplatesRequest.t" j
+
+
end
+
+
(* Resources/Templates/List Response *)
+
module ListResourceTemplatesResult = struct
+
module ResourceTemplate = struct
+
type t = {
+
uri_template: string;
+
name: string;
+
description: string option;
+
mime_type: string option;
+
}
+
+
let yojson_of_t { uri_template; name; description; mime_type } =
+
let assoc = [
+
("uriTemplate", `String uri_template);
+
("name", `String name);
+
] in
+
let assoc = match description with
+
| Some desc -> ("description", `String desc) :: assoc
+
| None -> assoc
+
in
+
let assoc = match mime_type with
+
| Some mime -> ("mimeType", `String mime) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields as json ->
+
let uri_template = match List.assoc_opt "uriTemplate" fields with
+
| Some (`String s) -> s
+
| _ -> Util.json_error "Missing or invalid 'uriTemplate' field" json
+
in
+
let name = match List.assoc_opt "name" fields with
+
| Some (`String s) -> s
+
| _ -> Util.json_error "Missing or invalid 'name' field" json
+
in
+
let description = List.assoc_opt "description" fields |> Option.map (function
+
| `String s -> s
+
| j -> Util.json_error "Expected string for description" j
+
) in
+
let mime_type = List.assoc_opt "mimeType" fields |> Option.map (function
+
| `String s -> s
+
| j -> Util.json_error "Expected string for mimeType" j
+
) in
+
{ uri_template; name; description; mime_type }
+
| j -> Util.json_error "Expected object for ListResourceTemplatesResult.ResourceTemplate.t" j
+
end
+
+
type t = {
+
resource_templates: ResourceTemplate.t list;
+
next_cursor: Cursor.t option;
+
}
+
+
let yojson_of_t { resource_templates; next_cursor } =
+
let assoc = [
+
("resourceTemplates", `List (List.map ResourceTemplate.yojson_of_t resource_templates));
+
] in
+
let assoc = match next_cursor with
+
| Some c -> ("nextCursor", Cursor.yojson_of_t c) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
let t_of_yojson = function
+
| `Assoc fields as json ->
+
let resource_templates = match List.assoc_opt "resourceTemplates" fields with
+
| Some (`List items) -> List.map ResourceTemplate.t_of_yojson items
+
| _ -> Util.json_error "Missing or invalid 'resourceTemplates' field" json
+
in
+
let next_cursor = List.assoc_opt "nextCursor" fields |> Option.map Cursor.t_of_yojson in
+
{ resource_templates; next_cursor }
+
| j -> Util.json_error "Expected object for ListResourceTemplatesResult.t" j
+
+
(* Request/response creation helpers *)
+
let create_request ?cursor ?id () =
+
let id = match id with
+
| Some i -> i
+
| None -> `Int (Random.int 10000)
+
in
+
let params = ListResourceTemplatesRequest.yojson_of_t { cursor } in
+
JSONRPCMessage.create_request ~id ~meth:Method.ResourceTemplatesList ~params:(Some params) ()
+
+
let create_response ~id ~resource_templates ?next_cursor () =
+
let result = yojson_of_t { resource_templates; next_cursor } in
+
JSONRPCMessage.create_response ~id ~result
+
end
+
(* Resources/Read *)
module ResourcesRead = struct
module Request = struct
+34
lib/mcp_rpc.mli
···
val create_response : id:RequestId.t -> resources:Resource.t list -> ?next_cursor:Cursor.t -> unit -> JSONRPCMessage.t
end
+
(** Resources/Templates/List - Request to list available resource templates *)
+
module ListResourceTemplatesRequest : sig
+
type t = {
+
cursor: Cursor.t option; (** Optional pagination cursor *)
+
}
+
include Json.Jsonable.S with type t := t
+
end
+
+
(** Resources/Templates/List - Response with resource templates *)
+
module ListResourceTemplatesResult : sig
+
(** Resource Template definition *)
+
module ResourceTemplate : sig
+
type t = {
+
uri_template: string; (** URI template for the resource *)
+
name: string; (** Human-readable name *)
+
description: string option; (** Optional description *)
+
mime_type: string option; (** Optional MIME type *)
+
}
+
include Json.Jsonable.S with type t := t
+
end
+
+
type t = {
+
resource_templates: ResourceTemplate.t list; (** List of available resource templates *)
+
next_cursor: Cursor.t option; (** Optional cursor for the next page *)
+
}
+
include Json.Jsonable.S with type t := t
+
+
(** Create a resources/templates/list request *)
+
val create_request : ?cursor:Cursor.t -> ?id:RequestId.t -> unit -> JSONRPCMessage.t
+
+
(** Create a resources/templates/list response *)
+
val create_response : id:RequestId.t -> resource_templates:ResourceTemplate.t list -> ?next_cursor:Cursor.t -> unit -> JSONRPCMessage.t
+
end
+
(** Resources/Read - Request to read resource contents *)
module ResourcesRead : sig
(** Request parameters *)
+91 -11
lib/mcp_sdk.ml
···
type handler = Context.t -> string list -> (string, string) result
type t = {
-
uri_template: string;
+
uri: string; (* For resources, this is the exact URI (no variables) *)
+
name: string;
description: string option;
mime_type: string option;
handler: handler;
}
-
let create ~uri_template ?description ?mime_type ~handler () =
-
{ uri_template; description; mime_type; handler }
+
let create ~uri ~name ?description ?mime_type ~handler () =
+
(* Validate that the URI doesn't contain template variables *)
+
if String.contains uri '{' || String.contains uri '}' then
+
Log.warningf "Resource '%s' contains template variables. Consider using add_resource_template instead." uri;
+
{ uri; name; description; mime_type; handler }
let to_json resource =
let assoc = [
-
("uriTemplate", `String resource.uri_template);
+
("uri", `String resource.uri);
+
("name", `String resource.name);
] in
let assoc = match resource.description with
| Some desc -> ("description", `String desc) :: assoc
···
(* Convert to Mcp_rpc.ResourcesList.Resource.t *)
let to_rpc_resource_list_resource (resource:t) =
Mcp_rpc.ResourcesList.Resource.{
-
uri = resource.uri_template;
-
name = resource.uri_template; (* Use uri as name by default *)
+
uri = resource.uri;
+
name = resource.name;
description = resource.description;
mime_type = resource.mime_type;
size = None; (* Size can be added when we have actual resource content *)
···
("required", required_json)
]
+
(* Resource Templates for the MCP server *)
+
module ResourceTemplate = struct
+
type handler = Context.t -> string list -> (string, string) result
+
+
type t = {
+
uri_template: string;
+
name: string;
+
description: string option;
+
mime_type: string option;
+
handler: handler;
+
}
+
+
let create ~uri_template ~name ?description ?mime_type ~handler () =
+
{ uri_template; name; description; mime_type; handler }
+
+
let to_json resource_template =
+
let assoc = [
+
("uriTemplate", `String resource_template.uri_template);
+
("name", `String resource_template.name);
+
] in
+
let assoc = match resource_template.description with
+
| Some desc -> ("description", `String desc) :: assoc
+
| None -> assoc
+
in
+
let assoc = match resource_template.mime_type with
+
| Some mime -> ("mimeType", `String mime) :: assoc
+
| None -> assoc
+
in
+
`Assoc assoc
+
+
(* Convert to Mcp_rpc.ResourceTemplatesList.ResourceTemplate.t *)
+
let to_rpc_resource_template (template:t) =
+
Mcp_rpc.ListResourceTemplatesResult.ResourceTemplate.{
+
uri_template = template.uri_template;
+
name = template.name;
+
description = template.description;
+
mime_type = template.mime_type;
+
}
+
+
(* Convert a list of ResourceTemplate.t to the format needed for resources/templates/list response *)
+
let to_rpc_resource_templates_list templates =
+
List.map to_rpc_resource_template templates
+
end
+
(* Main server type *)
type server = {
name: string;
···
mutable capabilities: Json.t;
mutable tools: Tool.t list;
mutable resources: Resource.t list;
+
mutable resource_templates: ResourceTemplate.t list;
mutable prompts: Prompt.t list;
}
···
let protocol_version { protocol_version; _ } = protocol_version
let tools { tools; _ } = tools
let resources { resources; _ } = resources
+
let resource_templates { resource_templates; _ } = resource_templates
let prompts { prompts; _ } = prompts
(* Create a new server *)
···
capabilities = `Assoc [];
tools = [];
resources = [];
+
resource_templates = [];
prompts = [];
lifespan_context = [];
}
(* Default capabilities for the server *)
-
let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_prompts=false) () =
+
let default_capabilities ?(with_tools=true) ?(with_resources=false) ?(with_resource_templates=false) ?(with_prompts=false) () =
let caps = [] in
let caps =
if with_tools then
···
resource
(* Create and register a resource in one step *)
-
let add_resource server ~uri_template ?description ?mime_type handler =
+
let add_resource server ~uri ~name ?description ?mime_type handler =
let handler' _ctx params =
try
Ok (handler params)
···
Error (Printexc.to_string exn)
in
let resource = Resource.create
+
~uri
+
~name
+
?description
+
?mime_type
+
~handler:handler'
+
()
+
in
+
register_resource server resource
+
+
(* Register a resource template *)
+
let register_resource_template server template =
+
server.resource_templates <- template :: server.resource_templates;
+
template
+
+
(* Create and register a resource template in one step *)
+
let add_resource_template server ~uri_template ~name ?description ?mime_type handler =
+
let handler' _ctx params =
+
try
+
Ok (handler params)
+
with exn ->
+
Error (Printexc.to_string exn)
+
in
+
let template = ResourceTemplate.create
~uri_template
+
~name
?description
?mime_type
~handler:handler'
()
in
-
register_resource server resource
+
register_resource_template server template
(* Register a prompt *)
let register_prompt server prompt =
···
server.capabilities <- capabilities
(* Configure server with default capabilities based on registered components *)
-
let configure_server server ?with_tools ?with_resources ?with_prompts () =
+
let configure_server server ?with_tools ?with_resources ?with_resource_templates ?with_prompts () =
let with_tools = match with_tools with
| Some b -> b
| None -> server.tools <> []
···
| Some b -> b
| None -> server.resources <> []
in
+
let with_resource_templates = match with_resource_templates with
+
| Some b -> b
+
| None -> server.resource_templates <> []
+
in
let with_prompts = match with_prompts with
| Some b -> b
| None -> server.prompts <> []
in
-
let capabilities = default_capabilities ~with_tools ~with_resources ~with_prompts () in
+
let capabilities = default_capabilities ~with_tools ~with_resources ~with_resource_templates ~with_prompts () in
set_capabilities server capabilities;
server
+32 -5
lib/mcp_sdk.mli
···
type handler = Context.t -> string list -> (string, string) result
type t = {
-
uri_template: string;
+
uri: string;
+
name: string;
description: string option;
mime_type: string option;
handler: handler;
}
-
val create : uri_template:string -> ?description:string -> ?mime_type:string -> handler:handler -> unit -> t
+
val create : uri:string -> name:string -> ?description:string -> ?mime_type:string -> handler:handler -> unit -> t
val to_json : t -> Json.t
(** Convert to Mcp_rpc.ResourcesList.Resource.t *)
···
val to_rpc_resources_list : t list -> Mcp_rpc.ResourcesList.Resource.t list
end
+
(** Resource Templates for the MCP server *)
+
module ResourceTemplate : sig
+
type handler = Context.t -> string list -> (string, string) result
+
+
type t = {
+
uri_template: string;
+
name: string;
+
description: string option;
+
mime_type: string option;
+
handler: handler;
+
}
+
+
val create : uri_template:string -> name:string -> ?description:string -> ?mime_type:string -> handler:handler -> unit -> t
+
val to_json : t -> Json.t
+
+
(** Convert to Mcp_rpc.ResourceTemplatesList.ResourceTemplate.t *)
+
val to_rpc_resource_template : t -> Mcp_rpc.ListResourceTemplatesResult.ResourceTemplate.t
+
+
(** Convert a list of ResourceTemplate.t to the format needed for resources/templates/list response *)
+
val to_rpc_resource_templates_list : t list -> Mcp_rpc.ListResourceTemplatesResult.ResourceTemplate.t list
+
end
+
(** Prompts for the MCP server *)
module Prompt : sig
type argument = {
···
val capabilities : server -> Json.t
val tools : server -> Tool.t list
val resources : server -> Resource.t list
+
val resource_templates : server -> ResourceTemplate.t list
val prompts : server -> Prompt.t list
(** Create a new server *)
val create_server : name:string -> ?version:string -> ?protocol_version:string -> unit -> server
(** Default capabilities for the server *)
-
val default_capabilities : ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> Json.t
+
val default_capabilities : ?with_tools:bool -> ?with_resources:bool -> ?with_resource_templates:bool -> ?with_prompts:bool -> unit -> Json.t
(** Create and register a tool in one step *)
val add_tool : server -> name:string -> ?description:string -> ?schema_properties:(string * string * string) list -> ?schema_required:string list -> (Json.t -> Json.t) -> Tool.t
(** Create and register a resource in one step *)
-
val add_resource : server -> uri_template:string -> ?description:string -> ?mime_type:string -> (string list -> string) -> Resource.t
+
val add_resource : server -> uri:string -> name:string -> ?description:string -> ?mime_type:string -> (string list -> string) -> Resource.t
+
+
(** Create and register a resource template in one step *)
+
val add_resource_template : server -> uri_template:string -> name:string -> ?description:string -> ?mime_type:string -> (string list -> string) -> ResourceTemplate.t
(** Create and register a prompt in one step *)
val add_prompt : server -> name:string -> ?description:string -> ?arguments:(string * string option * bool) list -> ((string * string) list -> Prompt.message list) -> Prompt.t
(** Configure server with default capabilities based on registered components *)
-
val configure_server : server -> ?with_tools:bool -> ?with_resources:bool -> ?with_prompts:bool -> unit -> server
+
val configure_server : server -> ?with_tools:bool -> ?with_resources:bool -> ?with_resource_templates:bool -> ?with_prompts:bool -> unit -> server
val make_tool_schema : (string * string * string) list -> string list -> Json.t
+100 -47
lib/mcp_server.ml
···
let response = Mcp_rpc.ResourcesList.create_response ~id:req.id ~resources:resources_list () in
Some response
+
(* Utility module for resource template matching *)
+
module Resource_matcher = struct
+
(* Define variants for resource handling result *)
+
type resource_match =
+
| DirectResource of Resource.t * string list
+
| TemplateResource of ResourceTemplate.t * string list
+
| NoMatch
+
+
(* Extract parameters from a template URI *)
+
let extract_template_vars template_uri uri =
+
(* Simple template variable extraction - could be enhanced with regex *)
+
let template_parts = String.split_on_char '/' template_uri in
+
let uri_parts = String.split_on_char '/' uri in
+
+
if List.length template_parts <> List.length uri_parts then
+
None
+
else
+
(* Match parts and extract variables *)
+
let rec match_parts tparts uparts acc =
+
match tparts, uparts with
+
| [], [] -> Some (List.rev acc)
+
| th::tt, uh::ut ->
+
(* Check if this part is a template variable *)
+
if String.length th > 2 &&
+
String.get th 0 = '{' &&
+
String.get th (String.length th - 1) = '}' then
+
(* Extract variable value and continue *)
+
match_parts tt ut (uh::acc)
+
else if th = uh then
+
(* Fixed part matches, continue *)
+
match_parts tt ut acc
+
else
+
(* Fixed part doesn't match, fail *)
+
None
+
| _, _ -> None
+
in
+
match_parts template_parts uri_parts []
+
+
(* Find a matching resource or template for a URI *)
+
let find_match server uri =
+
(* Try direct resource match first *)
+
match List.find_opt (fun resource -> resource.Resource.uri = uri) (resources server) with
+
| Some resource -> DirectResource (resource, [])
+
| None ->
+
(* Try template match next *)
+
let templates = resource_templates server in
+
+
(* Try each template to see if it matches *)
+
let rec try_templates templates =
+
match templates with
+
| [] -> NoMatch
+
| template::rest ->
+
match extract_template_vars template.ResourceTemplate.uri_template uri with
+
| Some params -> TemplateResource (template, params)
+
| None -> try_templates rest
+
in
+
try_templates templates
+
end
+
(* Process resources/read request *)
let handle_resources_read server (req:JSONRPCMessage.request) =
Log.debug "Processing resources/read request";
···
let uri = req_data.uri in
Log.debugf "Resource URI: %s" uri;
-
(* Find matching resource handler by URI template *)
-
let find_resource_handler uri =
-
let parse_uri_template template uri =
-
(* Simple URI template parsing: extract parameters from a URI based on a template *)
-
let template_parts = String.split_on_char '/' template in
-
let uri_parts = String.split_on_char '/' uri in
-
-
(* If parts don't match in length, this template doesn't match *)
-
if List.length template_parts <> List.length uri_parts then
-
None
-
else
-
(* Extract parameters where template has {param} format *)
-
let params, matches =
-
List.fold_left2 (fun (params, matches) template_part uri_part ->
-
if String.length template_part > 2 &&
-
template_part.[0] = '{' &&
-
template_part.[String.length template_part - 1] = '}' then
-
(* This is a parameter, extract its value *)
-
let _param_name = String.sub template_part 1 (String.length template_part - 2) in
-
(uri_part :: params, matches)
-
else if template_part = uri_part then
-
(* Constant part matches *)
-
(params, true && matches)
-
else
-
(* Constant part doesn't match *)
-
(params, false && matches)
-
) ([], true) template_parts uri_parts
-
in
-
-
if matches then Some (List.rev params) else None
-
in
-
-
let rec find_handler = function
-
| [] -> None
-
| resource :: rest ->
-
match parse_uri_template resource.Resource.uri_template uri with
-
| Some params -> Some (resource, params)
-
| None -> find_handler rest
-
in
-
-
find_handler (resources server)
-
in
-
-
match find_resource_handler uri with
-
| Some (resource, params) ->
+
(* Find matching resource or template *)
+
match Resource_matcher.find_match server uri with
+
| Resource_matcher.DirectResource (resource, params) ->
(* Create context for this request *)
let ctx = Context.create
?request_id:(Some req.id)
···
~lifespan_context:[("resources/read", `Assoc [("uri", `String uri)])]
()
in
+
+
Log.debugf "Handling direct resource: %s" resource.name;
(* Call the resource handler *)
(match resource.handler ctx params with
···
| Error err ->
Log.errorf "Error reading resource: %s" err;
Some (create_jsonrpc_error req.id ErrorCode.InternalError ("Error reading resource: " ^ err) ()))
-
| None ->
+
+
| Resource_matcher.TemplateResource (template, params) ->
+
(* Create context for this request *)
+
let ctx = Context.create
+
?request_id:(Some req.id)
+
?progress_token:req.progress_token
+
~lifespan_context:[("resources/read", `Assoc [("uri", `String uri)])]
+
()
+
in
+
+
Log.debugf "Handling resource template: %s with params: [%s]"
+
template.name
+
(String.concat ", " params);
+
+
(* Call the template handler *)
+
(match template.handler ctx params with
+
| Ok content ->
+
(* Create text resource content *)
+
let mime_type = match template.mime_type with
+
| Some mime -> mime
+
| None -> "text/plain"
+
in
+
let text_resource = {
+
TextResourceContents.uri;
+
text = content;
+
mime_type = Some mime_type
+
} in
+
let resource_content = Mcp_rpc.ResourcesRead.ResourceContent.TextResource text_resource in
+
let response = Mcp_rpc.ResourcesRead.create_response ~id:req.id ~contents:[resource_content] () in
+
Some response
+
| Error err ->
+
Log.errorf "Error reading resource template: %s" err;
+
Some (create_jsonrpc_error req.id ErrorCode.InternalError ("Error reading resource template: " ^ err) ()))
+
+
| Resource_matcher.NoMatch ->
Log.errorf "Resource not found: %s" uri;
Some (create_jsonrpc_error req.id ErrorCode.InvalidParams ("Resource not found: " ^ uri) ())
···
()
| exn ->
Log.errorf "Exception while reading: %s" (Printexc.to_string exn);
-
()
+
()