// Code generated by scripts/generate-mbt-picogk-sdk.py — DO NOT EDIT.
///|
/// Client manages a connection to the PicoGK MCP server.
/// It launches the server as a subprocess and communicates via JSON-RPC over stdio.
/// All tool methods are async and must be called within an `async fn main` block.
///
/// Usage:
/// async fn main {
/// // Uses default server at $HOME/.local/bin/picogk-mcp/PicoGK.Mcp
/// let client = @picogk.new_client("")
/// // Or specify a custom path:
/// // let client = @picogk.new_client("/custom/path/to/PicoGK.Mcp")
/// client.picogk_init(Some(0.5))
/// client.create_sphere(0.0, 0.0, 0.0, 30.0, Some("body"))
/// client.close()
/// }
///
pub struct Client {
pid : Int
stdin : @process.WriteToProcess
stdout : @process.ReadFromProcess
mut msg_id : Int
}
///|
/// default_server_bin returns the default path to the PicoGK MCP server binary.
///
let default_server_bin : String = "~/.local/bin/picogk-mcp/PicoGK.Mcp"
///|
/// VERSION is the semver version of this package.
///
const VERSION : String = "0.2.0"
///|
/// new_client launches the PicoGK MCP server binary and returns a Client.
/// If server_bin is empty, uses default_server_bin.
/// Must be called within an async function.
///
pub async fn new_client(server_bin : String) -> Client {
let bin = if server_bin == "" { default_server_bin } else { server_bin }
let path = expand_path(bin)
// Create pipes for stdin/stdout
let (stdin_read, stdin_write) = @process.write_to_process()
let (stdout_read, stdout_write) = @process.read_from_process()
// Spawn the server as an orphan process so it survives beyond any task group
let pid = @process.spawn_orphan(
path,
[],
stdin=stdin_read,
stdout=stdout_write,
)
let client = Client::{
pid,
stdin: stdin_write,
stdout: stdout_read,
msg_id: 0,
}
// Perform MCP initialize handshake
client.initialize()
client
}
///|
/// close shuts down the MCP server process.
///
pub fn Client::close(self : Client) -> Unit {
self.stdin.close()
self.stdout.close()
}
///|
/// expand_path expands a leading ~ to the user's home directory.
///
fn expand_path(path : String) -> String {
if path.has_prefix("~") {
let home = @env.get_env_var("HOME").unwrap_or("")
home + path[1:].to_owned()
} else {
path
}
}
///|
/// resolve_path resolves a path to an absolute path.
/// If the path starts with ~, it expands to the home directory.
/// If the path is already absolute (starts with /), it is returned as-is.
/// Otherwise, it is prepended with the current working directory.
///
fn resolve_path(path : String) -> String {
let expanded = expand_path(path)
if expanded.has_prefix("/") {
expanded
} else {
let cwd = @env.get_env_var("PWD").unwrap_or(".")
cwd + "/" + expanded
}
}
// --- JSON-RPC internals ---
///|
/// next_id returns the next JSON-RPC request ID.
///
fn Client::next_id(self : Client) -> Int {
self.msg_id = self.msg_id + 1
self.msg_id
}
///|
/// send_line writes a JSON-RPC message to the server's stdin.
///
async fn Client::send_line(self : Client, json_str : String) -> Unit {
let data = @utf8.encode(json_str + "\n")
let _ = self.stdin.write_once(data, offset=0, len=data.length())
}
///|
/// read_line reads a JSON-RPC response line from the server's stdout.
///
async fn Client::read_line(self : Client) -> String {
match self.stdout.read_until("\n") {
Some(line) => line
None => raise Failure::Failure("EOF: no more data from server")
}
}
///|
/// call_tool invokes an MCP tool by name and returns the text response.
///
pub async fn Client::call_tool(
self : Client,
tool_name : String,
args : Map[String, Json],
) -> String {
let id = self.next_id()
let req = Json::object(
Map([
("jsonrpc", Json::string("2.0")),
("id", Json::number(id.to_double())),
("method", Json::string("tools/call")),
(
"params",
Json::object(
Map([
("name", Json::string(tool_name)),
("arguments", Json::object(args)),
]),
),
),
]),
)
self.send_line(req.stringify())
let response = self.read_line()
// Parse response and extract text content
// Some tools (e.g. picogk_shutdown) may return non-JSON; return raw text in that case
try @json.parse(response) catch {
_ => return response
} noraise {
resp =>
match resp {
Object(fields) => {
let result = fields.get("result").unwrap()
match result {
Object(result_fields) => {
let content = result_fields.get("content").unwrap()
match content {
Array(items) => {
let mut text = ""
for item in items {
match item {
Object(item_fields) =>
match item_fields.get("text") {
Some(String(s)) => text = text + s
_ => ()
}
_ => ()
}
}
text
}
_ => raise Failure::Failure("unexpected content format")
}
}
_ => raise Failure::Failure("unexpected result format")
}
}
_ => response
}
}
}
///|
/// initialize performs the MCP initialize handshake.
///
async fn Client::initialize(self : Client) -> Unit {
let id = self.next_id()
let req = Json::object(
Map([
("jsonrpc", Json::string("2.0")),
("id", Json::number(id.to_double())),
("method", Json::string("initialize")),
(
"params",
Json::object(
Map([
("protocolVersion", Json::string("2024-11-05")),
("capabilities", Json::object(Map([]))),
(
"clientInfo",
Json::object(
Map([
("name", Json::string("picogk")),
("version", Json::string(VERSION)),
]),
),
),
]),
),
),
]),
)
self.send_line(req.stringify())
let _ = self.read_line()
// Send initialized notification
let notif = Json::object(
Map([
("jsonrpc", Json::string("2.0")),
("method", Json::string("notifications/initialized")),
]),
)
self.send_line(notif.stringify())
}