///|
fn client_capabilities_to_json(
capabilities : @types.ClientCapabilities,
) -> Json {
capabilities.to_json()
}
///|
/// Build the `_meta` object every modern request carries in its `params`.
/// Contains the required `io.modelcontextprotocol/protocolVersion` and
/// `clientCapabilities`, plus the recommended `clientInfo`. The optional
/// `logLevel` maps to the per-request `_meta` field that replaced the removed
/// `logging/setLevel` RPC — note the Logging feature itself is Deprecated by
/// the spec (SEP-2577: new implementations should not add support), so this
/// exists for interop with servers that still honor it, and nothing in this
/// SDK reads or emits log-level-gated messages.
fn build_request_meta(
client_name : String,
client_version : String,
capabilities : @types.ClientCapabilities,
log_level~ : String? = None,
) -> Json {
let meta : Map[String, Json] = Default::default()
meta.set(
"io.modelcontextprotocol/protocolVersion",
Json::string(@types.ProtocolVersion),
)
meta.set(
"io.modelcontextprotocol/clientCapabilities",
client_capabilities_to_json(capabilities),
)
meta.set("io.modelcontextprotocol/clientInfo", {
"name": client_name,
"version": client_version,
})
match log_level {
Some(level) =>
meta.set("io.modelcontextprotocol/logLevel", Json::string(level))
None => ()
}
Json::object(meta)
}
///|
fn build_tools_list_request(
id : Int32,
meta : Json,
cursor~ : String = "",
) -> String {
let params : Map[String, Json] = Default::default()
params.set("_meta", meta)
if cursor != "" {
params.set("cursor", Json::string(cursor))
}
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "tools/list",
"params": Json::object(params),
}).stringify()
}
///|
fn parse_argument_object(arguments : String) -> Json raise @types.MCPError {
let value = @validation.parse_json(arguments) catch {
_ => raise @types.InvalidParams("Arguments must be valid JSON")
}
guard value is Object(_) else {
raise @types.InvalidParams("Arguments must be a JSON object")
}
value
}
///|
fn build_tools_call_request(
id : Int32,
name : String,
meta : Json,
arguments~ : String = "{}",
progress_token? : @types.ProgressToken,
) -> String raise @types.MCPError {
let arguments_json = parse_argument_object(arguments)
let params_map : Map[String, Json] = Default::default()
params_map.set("name", Json::string(name))
params_map.set("arguments", arguments_json)
// Merge progressToken into the request _meta alongside protocol-level fields.
match meta {
Object(obj) => {
let fields = obj.copy()
match progress_token {
Some(token) => {
let value = token.to_json()
ignore(@types.ProgressToken::from_json(value))
fields.set("progressToken", value)
}
None => ()
}
params_map.set("_meta", Json::object(fields))
}
_ => params_map.set("_meta", meta)
}
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "tools/call",
"params": Json::object(params_map),
}).stringify()
}
///|
fn build_resources_list_request(
id : Int32,
meta : Json,
cursor~ : String = "",
) -> String {
let params : Map[String, Json] = Default::default()
params.set("_meta", meta)
if cursor != "" {
params.set("cursor", Json::string(cursor))
}
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "resources/list",
"params": Json::object(params),
}).stringify()
}
///|
fn build_resources_read_request(
id : Int32,
uri : String,
meta : Json,
) -> String {
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "resources/read",
"params": { "uri": uri, "_meta": meta },
}).stringify()
}
///|
fn build_prompts_list_request(
id : Int32,
meta : Json,
cursor~ : String = "",
) -> String {
let params : Map[String, Json] = Default::default()
params.set("_meta", meta)
if cursor != "" {
params.set("cursor", Json::string(cursor))
}
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "prompts/list",
"params": Json::object(params),
}).stringify()
}
///|
fn build_prompts_get_request(
id : Int32,
name : String,
meta : Json,
arguments~ : String = "{}",
) -> String raise @types.MCPError {
let arguments_json = parse_argument_object(arguments)
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "prompts/get",
"params": { "name": name, "arguments": arguments_json, "_meta": meta },
}).stringify()
}
///|
fn build_resources_templates_list_request(
id : Int32,
meta : Json,
cursor~ : String = "",
) -> String {
let params : Map[String, Json] = Default::default()
params.set("_meta", meta)
if cursor != "" {
params.set("cursor", Json::string(cursor))
}
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "resources/templates/list",
"params": Json::object(params),
}).stringify()
}
///|
fn build_completion_reference_request(
id : Int32,
meta : Json,
reference : @types.CompletionReference,
argument_name~ : String,
argument_value~ : String,
context_arguments? : Map[String, String],
) -> String {
let context = context_arguments.map(fn(arguments) {
@types.CompletionContext::new(arguments~)
})
let params = @types.CompleteRequestParams::new(
reference,
@types.CompletionArgument::new(argument_name, argument_value),
context?,
).to_json()
guard params is Object(fields) else {
abort("Completion params must be an object")
}
fields.set("_meta", meta)
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "completion/complete",
"params": Json::object(fields),
}).stringify()
}
///|
/// Build a `subscriptions/listen` request. Per the 2026-07-28 spec
/// (basic/patterns/subscriptions), `params.notifications` is a boolean
/// opt-in object (SubscriptionFilter), not a list of method names; each
/// field is emitted only when the filter enables it, and
/// `resourceSubscriptions` nests inside `notifications` and is omitted
/// when empty. This mirrors the server-side `parse_subscription_filter`
/// semantics: absent fields simply mean "not subscribed".
fn build_subscriptions_listen_request(
id : Int32,
meta : Json,
filter : ListenFilter,
) -> String {
let notifications : Map[String, Json] = Default::default()
if filter.tools_list_changed {
notifications.set("toolsListChanged", Json::boolean(true))
}
if filter.prompts_list_changed {
notifications.set("promptsListChanged", Json::boolean(true))
}
if filter.resources_list_changed {
notifications.set("resourcesListChanged", Json::boolean(true))
}
if filter.resource_subscriptions.length() > 0 {
notifications.set(
"resourceSubscriptions",
Json::array(filter.resource_subscriptions.map(Json::string)),
)
}
let params : Map[String, Json] = Default::default()
params.set("notifications", Json::object(notifications))
params.set("_meta", meta)
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "subscriptions/listen",
"params": Json::object(params),
}).stringify()
}
///|
/// Build a `server/discover` request. Per the 2026-07-28 spec the request
/// carries no parameters beyond the standard `_meta`; it is the primary way
/// clients learn a server's supported versions, capabilities, and identity,
/// and serves as the era probe for legacy fallback.
fn build_discover_request(id : Int32, meta : Json) -> String {
Json::object({
"jsonrpc": "2.0",
"id": id,
"method": "server/discover",
"params": { "_meta": meta },
}).stringify()
}
///|
using @transport {type HeaderSchemaValidation}
///|
fn validate_tool_header_schema(input_schema : Json) -> HeaderSchemaValidation {
@transport.validate_tool_header_schema(input_schema)
}