///|
/// Turning parsed results into the text or JSON a caller sees, plus the
/// schema-aware coercion that makes `key=value` arguments pleasant to write.
///|
/// Parse an integer, or `None` when the text is not one.
fn try_int(raw : String) -> Int32? {
Some(@string.parse_int(raw.trim())) catch {
_ => None
}
}
///|
/// Parse a finite double, or `None` when the text is not one. Non-JSON numeric
/// tokens such as `Infinity` and `NaN` stay strings: serialising them as JSON
/// numbers would emit invalid JSON (or, in JavaScript, `null`).
fn try_double(raw : String) -> Double? {
let value : Double = @string.parse_double(raw.trim()) catch {
_ => return None
}
if value.is_nan() || value.is_inf() {
None
} else {
Some(value)
}
}
///|
/// Coerce a `key=value` argument using the tool's schema. This is where mcpbox
/// improves on a plain string passthrough: `count=5` becomes a JSON number and
/// `tags=a,b` a JSON array when the schema says so, so callers do not have to
/// hand-write JSON for ordinary values. Anything that does not parse falls back
/// to the string, and `key:=` is always available for the exact form.
pub fn coerce_value(raw : String, schema : Json?) -> Json {
match schema {
None => Json::string(raw)
Some(definition) =>
match schema_type(definition) {
"integer" =>
match try_int(raw) {
Some(value) =>
Json::number(value.to_double(), repr=value.to_string())
None => Json::string(raw)
}
"number" =>
match try_double(raw) {
Some(value) => Json::number(value)
None => Json::string(raw)
}
"boolean" =>
match raw.trim() {
"true" => Json::boolean(true)
"false" => Json::boolean(false)
_ => Json::string(raw)
}
"array" => {
let item_schema = match definition {
Object(obj) => obj.get("items")
_ => None
}
let items : Array[Json] = []
for item in raw.split(",") {
items.push(coerce_value(item.trim().to_owned(), item_schema))
}
Json::array(items)
}
"object" => @json.parse(raw) catch { _ => Json::string(raw) }
_ => Json::string(raw)
}
}
}
///|
/// Build the `arguments` object for a tool call.
pub fn build_arguments(
pairs : Array[String],
schema : Json,
) -> Json raise McpError {
let properties = schema_properties(schema)
let fields : Map[String, Json] = Map([])
for pair in pairs {
match pair.find(":=") {
Some(index) => {
let key = pair.exact_view(end=index).to_owned()
let raw = pair.exact_view(start=index + 2).to_owned()
if key.length() == 0 {
raise fail("'" + pair + "' has an empty key")
}
fields[key] = parse_jsonc(raw)
}
None =>
match pair.find("=") {
Some(index) => {
let key = pair.exact_view(end=index).to_owned()
let raw = pair.exact_view(start=index + 1).to_owned()
if key.length() == 0 {
raise fail("'" + pair + "' has an empty key")
}
fields[key] = coerce_value(raw, properties.get(key))
}
None => raise fail("'" + pair + "' is not a key=value argument")
}
}
}
Json::object(fields)
}
///|
/// The JSON view of a server's `initialize` result.
pub fn server_info_to_json(info : ServerInfo) -> Json {
let fields : Map[String, Json] = {
"name": Json::string(info.name),
"version": Json::string(info.version),
"protocolVersion": Json::string(info.protocol_version),
}
if info.instructions.length() > 0 {
fields["instructions"] = Json::string(info.instructions)
}
Json::object(fields)
}
///|
/// The names-only discovery view. This path never connects to a server.
fn render_server_names(servers : Array[Server]) -> String {
let names : Array[String] = []
for server in servers {
names.push(server.name)
}
names.join("\n")
}
///|
/// The JSON view of configured servers without contacting any of them.
fn render_servers_json(servers : Array[Server]) -> String {
let items : Array[Json] = []
for server in servers {
items.push(server_to_json(server))
}
Json::object({ "servers": Json::array(items) }).stringify(indent=2)
}
///|
/// Render the `mcpbox list` result as JSON.
fn render_reports_json(reports : Array[ServerReport]) -> String {
let servers : Array[Json] = []
for report in reports {
let fields : Map[String, Json] = match server_to_json(report.server) {
Object(obj) => obj
_ => Map([])
}
match report.info {
Some(info) => fields["serverInfo"] = server_info_to_json(info)
None => ()
}
match report.error {
Some(message) => fields["error"] = Json::string(message)
None => ()
}
let tools : Array[Json] = []
for tool in report.tools {
tools.push(tool_to_json(tool))
}
fields["tools"] = Json::array(tools)
servers.push(Json::object(fields))
}
Json::object({ "servers": Json::array(servers) }).stringify(indent=2)
}
///|
/// Render the `mcpbox list` result for a human.
fn render_reports_text(reports : Array[ServerReport], opts : Options) -> String {
let out = StringBuilder()
let mut first = true
for report in reports {
if !first {
out.write_string("\n")
}
first = false
if opts.brief {
out.write_string("\{report.server.name} ")
match report.error {
Some(message) => out.write_string("error: " + message)
None => out.write_string("(\{report.tools.length()} tools)")
}
continue
}
out.write_string(
"\{report.server.name} \{transport_target(report.server)}",
)
match report.info {
Some(info) =>
if info.name.length() > 0 || info.version.length() > 0 {
out.write_string(" [\{info.name} \{info.version}]")
}
None => ()
}
out.write_string("\n")
if !opts.no_docs {
match report.info {
Some(info) =>
for line in render_instructions(info.instructions, " ") {
out.write_string(line + "\n")
}
None => ()
}
}
match report.error {
Some(message) => out.write_string(" error: " + message + "\n")
None =>
if report.tools.length() == 0 {
out.write_string(" (no tools)\n")
} else {
let mut first_tool = true
for tool in report.tools {
if !first_tool && !opts.no_docs {
out.write_string("\n")
}
first_tool = false
if !opts.no_docs {
for line in render_tool_doc(tool, " ") {
out.write_string(line + "\n")
}
}
out.write_string(" " + render_tool_signature(tool) + "\n")
}
}
}
}
out.to_string()
}
///|
/// Render a tool call result according to the output flags.
fn render_call_output(result : CallResult, opts : Options) -> String {
if opts.json {
return call_result_to_json(result).stringify()
}
if opts.raw {
let out = StringBuilder()
for block in result.content {
match block {
Text(text) => out.write_string(text)
_ => ()
}
}
return out.to_string()
}
render_call_result(result)
}