///|
pub struct ToolAnnotations {
title : String?
read_only_hint : Bool?
destructive_hint : Bool?
idempotent_hint : Bool?
open_world_hint : Bool?
} derive(Debug, Eq)
///|
/// Construct optional tool behavior annotations.
pub fn ToolAnnotations::ToolAnnotations(
title? : String,
read_only_hint? : Bool,
destructive_hint? : Bool,
idempotent_hint? : Bool,
open_world_hint? : Bool,
) -> ToolAnnotations {
{ title, read_only_hint, destructive_hint, idempotent_hint, open_world_hint }
}
///|
pub impl ToJson for ToolAnnotations with fn to_json(self : ToolAnnotations) -> Json {
let fields : Map[String, Json] = Map([])
fields.set_opt("title", self.title.map(Json::string))
fields.set_opt("readOnlyHint", self.read_only_hint.map(Json::boolean))
fields.set_opt("destructiveHint", self.destructive_hint.map(Json::boolean))
fields.set_opt("idempotentHint", self.idempotent_hint.map(Json::boolean))
fields.set_opt("openWorldHint", self.open_world_hint.map(Json::boolean))
Json::object(fields)
}
///|
pub impl @json.FromJson for ToolAnnotations with fn from_json(
value : Json,
path : @json.JsonPath,
) -> ToolAnnotations raise {
guard value is Object(fields) else {
raise JsonDecodeError((path, "expected object"))
}
let title = match fields.get("title") {
Some(String(s)) => Some(s)
Some(Null) | None => None
Some(_) => raise JsonDecodeError((path.add_key("title"), "expected string"))
}
let read_bool_opt = fn(key : String) -> Bool? raise @json.JsonDecodeError {
match fields.get(key) {
Some(True) => Some(true)
Some(False) => Some(false)
Some(Null) | None => None
Some(_) => raise JsonDecodeError((path.add_key(key), "expected boolean"))
}
}
let read_only_hint = read_bool_opt("readOnlyHint")
let destructive_hint = read_bool_opt("destructiveHint")
let idempotent_hint = read_bool_opt("idempotentHint")
let open_world_hint = read_bool_opt("openWorldHint")
ToolAnnotations(
title?,
read_only_hint?,
destructive_hint?,
idempotent_hint?,
open_world_hint?,
)
}
///|
pub fn ToolAnnotations::from_json(
value : Json,
path? : @json.JsonPath,
) -> ToolAnnotations raise McpError {
decode_json(value, path?)
}
///|
/// MCP tool descriptor.
pub struct Tool {
name : String
title : String?
description : String?
input_schema : Json
output_schema : Json?
annotations : ToolAnnotations?
icons : Array[Icon]
meta : Map[String, Json]
} derive(Debug, Eq)
///|
/// Copy a tool descriptor before handing it to an adapter.
pub fn Tool::copy(self : Tool) -> Tool {
{
..self,
input_schema: self.input_schema.copy(),
output_schema: self.output_schema.map(fn(s) { s.copy() }),
icons: self.icons.map(icon => icon.copy()),
meta: self.meta.copy_json(),
}
}
///|
/// Construct a tool descriptor.
pub fn Tool::Tool(
name~ : String,
input_schema~ : Json,
title? : String,
description? : String,
output_schema? : Json,
annotations? : ToolAnnotations,
icons? : Array[Icon] = [],
meta? : Map[String, Json] = {},
) -> Tool raise McpError {
let _ = @schema.Schema::raw(input_schema) catch {
_ =>
raise InvalidParams(
"tool inputSchema is not a valid JSON Schema document",
)
}
guard input_schema is Object(fields) else {
raise InvalidParams("tool inputSchema must be a JSON Schema object")
}
guard fields.get("type") is Some(String("object")) else {
raise InvalidParams("tool inputSchema root type must be \"object\"")
}
if output_schema is Some(schema) {
let _ = @schema.Schema::raw(schema) catch {
_ =>
raise InvalidParams(
"tool outputSchema is not a valid JSON Schema document",
)
}
}
validate_meta_map(meta)
{
name,
title,
description,
input_schema: input_schema.copy(),
output_schema: output_schema.map(fn(s) { s.copy() }),
annotations,
icons: icons.map(icon => icon.copy()),
meta: meta.copy_json(),
}
}
///|
pub impl ToJson for Tool with fn to_json(self : Tool) -> Json {
let fields : Map[String, Json] = {
"name": Json::string(self.name),
"inputSchema": self.input_schema.copy(),
}
fields.set_opt("title", self.title.map(Json::string))
fields.set_opt("description", self.description.map(Json::string))
fields.set_opt("outputSchema", self.output_schema.map(fn(s) { s.copy() }))
match self.annotations {
Some(annotations) => fields["annotations"] = ToJson::to_json(annotations)
None => ()
}
if !self.icons.is_empty() {
fields["icons"] = self.icons
.map(fn(icon) { ToJson::to_json(icon) })
.to_json()
}
if !self.meta.is_empty() {
fields["_meta"] = Json::object(self.meta.copy_json())
}
Json::object(fields)
}
///|
pub impl @json.FromJson for Tool with fn from_json(
value : Json,
path : @json.JsonPath,
) -> Tool raise {
guard value is Object(fields) else {
raise JsonDecodeError((path, "expected object"))
}
let name = match fields.get("name") {
Some(String(s)) => s
Some(_) => raise JsonDecodeError((path.add_key("name"), "expected string"))
None => raise JsonDecodeError((path.add_key("name"), "name is required"))
}
let title = match fields.get("title") {
Some(String(s)) => Some(s)
Some(Null) | None => None
Some(_) => raise JsonDecodeError((path.add_key("title"), "expected string"))
}
let description = match fields.get("description") {
Some(String(s)) => Some(s)
Some(Null) | None => None
Some(_) =>
raise JsonDecodeError((path.add_key("description"), "expected string"))
}
guard fields.get("inputSchema") is Some(input_schema) else {
raise JsonDecodeError(
(path.add_key("inputSchema"), "inputSchema is required"),
)
}
let output_schema = fields.get("outputSchema")
let annotations : ToolAnnotations? = match fields.get("annotations") {
Some(v) => Some(@json.from_json(v, path=path.add_key("annotations")))
None => None
}
let icons : Array[Icon] = match fields.get("icons") {
Some(Array(arr)) => {
let result : Array[Icon] = []
for i, it in arr {
result.push(
@json.from_json(it, path=path.add_key("icons").add_index(i)),
)
}
result
}
Some(Null) | None => []
Some(_) => raise JsonDecodeError((path.add_key("icons"), "expected array"))
}
let meta : Map[String, Json] = match fields.get("_meta") {
Some(Object(m)) => {
validate_meta_map(m) catch {
error => raise JsonDecodeError((path.add_key("_meta"), error.message()))
}
m.copy_json()
}
Some(Null) | None => Map([])
Some(_) => raise JsonDecodeError((path.add_key("_meta"), "expected object"))
}
Tool(
name~,
title?,
description?,
input_schema~,
output_schema?,
annotations?,
icons~,
meta~,
) catch {
error => raise JsonDecodeError((path, error.message()))
}
}
///|
pub fn Tool::from_json(
value : Json,
path? : @json.JsonPath,
) -> Tool raise McpError {
decode_json(value, path?)
}
///|
pub struct CallToolResult {
content : Array[Content]
structured_content : Json?
is_error : Bool
meta : ResultMeta?
} derive(Debug, Eq)
///|
/// Construct a tool result with validated ownership of its content array.
pub fn CallToolResult::CallToolResult(
content~ : Array[Content],
structured_content? : Json,
is_error? : Bool = false,
meta? : ResultMeta,
) -> CallToolResult {
{
content: content.copy(),
structured_content: structured_content.map(fn(c) { c.copy() }),
is_error,
meta: meta.map(fn(m) { m.copy() }),
}
}
///|
/// Decode the optional structured content as the caller's domain type.
pub fn[T : FromJson] CallToolResult::decode_structured(
self : CallToolResult,
) -> T raise McpError {
match self.structured_content {
Some(value) =>
@json.from_json(value) catch {
error => raise InvalidParams(error.to_string())
}
None => raise InvalidRequest("tools/call result has no structuredContent")
}
}
///|
pub impl ToJson for CallToolResult with fn to_json(self : CallToolResult) -> Json {
let fields : Map[String, Json] = {
"content": self.content.map(fn(block) { ToJson::to_json(block) }).to_json(),
"isError": Json::boolean(self.is_error),
}
fields.set_opt(
"structuredContent",
self.structured_content.map(fn(c) { c.copy() }),
)
if self.meta is Some(meta) {
fields["_meta"] = ToJson::to_json(meta)
}
Json::object(fields)
}
///|
pub impl @json.FromJson for CallToolResult with fn from_json(
value : Json,
path : @json.JsonPath,
) -> CallToolResult raise {
guard value is Object(fields) else {
raise JsonDecodeError((path, "expected object"))
}
let content : Array[Content] = match fields.get("content") {
Some(Array(arr)) => {
let result : Array[Content] = []
for i, it in arr {
result.push(
@json.from_json(it, path=path.add_key("content").add_index(i)),
)
}
result
}
Some(Null) | None => []
Some(_) =>
raise JsonDecodeError((path.add_key("content"), "expected array"))
}
let structured_content = fields.get("structuredContent")
let is_error = match fields.get("isError") {
Some(True) => true
Some(False | Null) | None => false
Some(_) =>
raise JsonDecodeError((path.add_key("isError"), "expected boolean"))
}
let meta : ResultMeta? = match fields.get("_meta") {
Some(v) => Some(@json.from_json(v, path=path.add_key("_meta")))
None => None
}
CallToolResult(content~, structured_content?, is_error~, meta?)
}
///|
pub fn CallToolResult::from_json(
value : Json,
path? : @json.JsonPath,
) -> CallToolResult raise McpError {
decode_json(value, path?)
}
///|
/// Return a plain text tool result.
pub fn CallToolResult::text(
text : String,
annotations? : Annotations,
meta? : Json,
) -> CallToolResult {
CallToolResult(content=[Content::text(text, annotations?, meta?)])
}
///|
/// Return structured content without a text block.
pub fn CallToolResult::json(value : Json) -> CallToolResult {
CallToolResult(content=[], structured_content=value)
}
///|
/// Return typed structured content through `ToJson`.
pub fn[T : ToJson] CallToolResult::structured(value : T) -> CallToolResult {
CallToolResult::json(value.to_json())
}
///|
/// Return a tool error as MCP content, not as a JSON-RPC protocol error.
pub fn CallToolResult::error_text(
text : String,
annotations? : Annotations,
meta? : Json,
) -> CallToolResult {
CallToolResult(
content=[Content::text(text, annotations?, meta?)],
is_error=true,
)
}
///|
/// Canonical decoded result of `tools/list`.
pub struct ListToolsResult {
tools : Array[Tool]
next_cursor : String?
cache_policy : CachePolicy
meta : ResultMeta?
} derive(Debug, Eq)
///|
pub impl ToJson for ListToolsResult with fn to_json(self : ListToolsResult) -> Json {
let fields : Map[String, Json] = {
"resultType": ToJson::to_json(ResultType::Complete),
"tools": self.tools.map(fn(t) { ToJson::to_json(t) }).to_json(),
}
fields.set_opt("nextCursor", self.next_cursor.map(Json::string))
self.cache_policy.put(fields)
if self.meta is Some(meta) {
fields["_meta"] = ToJson::to_json(meta)
}
Json::object(fields)
}
///|
pub impl @json.FromJson for ListToolsResult with fn from_json(
value : Json,
path : @json.JsonPath,
) -> ListToolsResult raise {
guard value is Object(fields) else {
raise JsonDecodeError((path, "expected object"))
}
let tools : Array[Tool] = match fields.get("tools") {
Some(Array(arr)) => {
let result : Array[Tool] = []
for i, it in arr {
result.push(
@json.from_json(it, path=path.add_key("tools").add_index(i)),
)
}
result
}
Some(_) => raise JsonDecodeError((path.add_key("tools"), "expected array"))
None => raise JsonDecodeError((path.add_key("tools"), "tools is required"))
}
let next_cursor = match fields.get("nextCursor") {
Some(String(s)) => Some(s)
Some(Null) | None => None
Some(_) =>
raise JsonDecodeError((path.add_key("nextCursor"), "expected string"))
}
let cache_policy : CachePolicy = @json.from_json(value) catch {
JsonDecodeError((_, msg)) => raise JsonDecodeError((path, msg))
}
let meta : ResultMeta? = match fields.get("_meta") {
Some(v) => Some(@json.from_json(v, path=path.add_key("_meta")))
None => None
}
{ tools, next_cursor, cache_policy, meta }
}
///|
pub fn ListToolsResult::from_json(
value : Json,
path? : @json.JsonPath,
) -> ListToolsResult raise McpError {
decode_json(value, path?)
}
///|
/// Parameters for `tools/call`.
pub struct CallToolParams {
name : String
arguments : Json
} derive(Debug, Eq)
///|
pub fn CallToolParams::CallToolParams(
name~ : String,
arguments? : Json = Json::object({}),
) -> CallToolParams raise McpError {
guard !name.is_empty() else {
raise InvalidParams("tool name must not be empty")
}
let args = match arguments {
Object(_) as obj => obj
Null => Json::object({})
_ => raise InvalidParams("tools/call params.arguments must be an object")
}
{ name, arguments: args }
}
///|
pub impl @json.FromJson for CallToolParams with fn from_json(
value : Json,
path : @json.JsonPath,
) -> CallToolParams raise {
guard value is Object(fields) else {
raise JsonDecodeError((path, "expected object"))
}
let name = match fields.get("name") {
Some(String(name)) if !name.is_empty() => name
Some(String(_)) =>
raise JsonDecodeError(
(path.add_key("name"), "tool name must not be empty"),
)
Some(_) => raise JsonDecodeError((path.add_key("name"), "expected string"))
None => raise JsonDecodeError((path.add_key("name"), "name is required"))
}
let arguments = match fields.get("arguments") {
Some(Object(_) as args) => args
Some(Null) | None => Json::object({})
Some(_) =>
raise JsonDecodeError((path.add_key("arguments"), "expected object"))
}
CallToolParams(name~, arguments~) catch {
error => raise JsonDecodeError((path, error.message()))
}
}
///|
pub impl ToJson for CallToolParams with fn to_json(self : CallToolParams) -> Json {
Json::object({ "name": Json::string(self.name), "arguments": self.arguments })
}
///|
pub fn CallToolParams::from_json(
value : Json,
path? : @json.JsonPath,
) -> CallToolParams raise McpError {
decode_json(value, path?)
}