Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions specs/port/trios/crates/trios-server/src/mcp.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
// trios-server MCP service — the tool catalog and the call wrapper.
// Port of gHashTag/trios:crates/trios-server/src/mcp.rs
// (at 8b229e9489ee, 338 lines) — Roadmap Stage 6, goal #4548, target t27c
// gen-rust.
//
// The original serves the trios tool catalog over MCP: `build_tool_definitions`
// declares the nineteen tools (git basic and extended, gb, fs, kg), `prop_type`
// and `prop_array_items` build the JSON-Schema property descriptors they
// carry, `McpService::list_tools` answers the catalog, and
// `McpService::call_tool` dispatches a call and wraps its result — a pretty
// JSON text for object and array results, the string itself for strings,
// compact JSON otherwise; an absent arguments map becomes JSON null; a
// failed dispatch answers one text block prefixed "Error: " with
// is_error set, a successful one the wrapped result with is_error unset.
//
// The serde_json maps, the LazyLock static and the dispatch await are
// plumbing; the decisions the file makes — the property descriptors' shape
// (a named type; an array of strings), the catalog's count and names, the
// per-tool facts (which properties are required, which tool reads the
// array descriptor, the git_log limit's integer type with its default of
// 10), the list answer's shape (no meta, no next cursor), the
// arguments-become-null rule, the result text kind selection, the error
// flag and prefix, and the single content block — are ported here as pure
// functions and tested. The McpService_call_tool transport keeps its name
// with an `undefined;` body that no test calls (issue #9563, "Port the
// decision, not the plumbing").
//
// Rust's `McpService` is a zero-sized struct; the port keeps the empty
// struct shape and the `McpService_new` constructor that builds it.

module port::trios_server_mcp;

// ---------------------------------------------------------------------------
// Original lines 13-24: `fn prop_type(type_str) -> Map` builds the JSON
// property descriptor {"type": <type_str>}; `fn prop_array_items() -> Map`
// builds {"type": "array", "items": {"type": "string"}}. The map keys are
// the descriptor's shape: a named type, or an array whose items are
// strings.
// ---------------------------------------------------------------------------

pub struct PropSchema {
type_name: str,
items_type_name: str,
is_array: bool,
}

pub fn prop_type(type_str: str) -> PropSchema {
return PropSchema { type_name: type_str, items_type_name: "", is_array: false };
}

pub fn prop_array_items() -> PropSchema {
return PropSchema { type_name: "array", items_type_name: "string", is_array: true };
}

// ---------------------------------------------------------------------------
// Original lines 26-280: `fn build_tool_definitions() -> Vec<Tool>`. The
// catalog: nineteen tools — nine git tools (status, stage_files,
// unstage_files, commit, create_branch, log, diff, stash, checkout), three
// gb tools (list_branches, push_stack, workspace_status), three fs tools
// (read_file, write_file, list_dir) and four kg tools (create_entity,
// create_edge, query, traverse). The growable Vec is plumbing; the count
// and the names are the decisions, ported below. The per-tool facts the
// original fixes: git_status requires one property (repo_path, a string)
// and describes itself as listing every changed file; git_stage_files
// takes two required properties with the paths property read through the
// array descriptor; git_log's limit property is an integer defaulting
// to 10.
// ---------------------------------------------------------------------------

pub fn build_tool_definitions() -> usize {
return 19;
}

pub fn tool_name(index: usize) -> str {
if (index == 0) { return "git_status"; }
if (index == 1) { return "git_stage_files"; }
if (index == 2) { return "git_unstage_files"; }
if (index == 3) { return "git_commit"; }
if (index == 4) { return "git_create_branch"; }
if (index == 5) { return "git_log"; }
if (index == 6) { return "git_diff"; }
if (index == 7) { return "git_stash"; }
if (index == 8) { return "git_checkout"; }
if (index == 9) { return "gb_list_branches"; }
if (index == 10) { return "gb_push_stack"; }
if (index == 11) { return "gb_workspace_status"; }
if (index == 12) { return "fs_read_file"; }
if (index == 13) { return "fs_write_file"; }
if (index == 14) { return "fs_list_dir"; }
if (index == 15) { return "kg_create_entity"; }
if (index == 16) { return "kg_create_edge"; }
if (index == 17) { return "kg_query"; }
if (index == 18) { return "kg_traverse"; }
return "";
}

pub fn git_status_description() -> str {
return "List all changed files in a repository";
}

pub fn git_status_required_count() -> usize {
return 1;
}

pub fn git_status_property_count() -> usize {
return 1;
}

pub fn git_stage_files_required_count() -> usize {
return 2;
}

pub fn git_stage_files_has_array_property() -> bool {
return true;
}

pub fn git_log_limit_property_type() -> str {
return "integer";
}

pub fn git_log_limit_default() -> usize {
return 10;
}

// ---------------------------------------------------------------------------
// Original lines 281-295: `impl McpService { pub fn new() -> Self; pub fn
// list_tools(&self) -> ListToolsResult }`. The service is a zero-sized
// struct; the list answer carries the whole catalog with no meta and no
// next cursor.
// ---------------------------------------------------------------------------

pub struct McpService { }

pub fn McpService_new() -> McpService {
return McpService { };
}

pub struct ListToolsView {
tool_count: usize,
meta_present: bool,
next_cursor_present: bool,
}

pub fn McpService_list_tools(service: McpService) -> ListToolsView {
return ListToolsView {
tool_count: build_tool_definitions(),
meta_present: false,
next_cursor_present: false,
};
}

// ---------------------------------------------------------------------------
// Original lines 297-337: `pub async fn call_tool(&self, params) ->
// CallToolResult`. Dispatches the named tool and wraps the result. The
// dispatch await is plumbing; the decisions are ported below: an absent
// arguments map becomes JSON null; a successful object or array result is
// rendered as pretty JSON text, a string result as the string itself,
// anything else as compact JSON; the answer is exactly one text content
// block; a failed dispatch prefixes the error text with "Error: " and sets
// is_error.
// ---------------------------------------------------------------------------

pub fn McpService_call_tool(service: McpService, tool_name: str, has_arguments: bool) -> ?str {
return undefined; // plumbing: dispatches to tools::dispatch and wraps the result (lines 297-337)
}

pub fn arguments_become_null(has_arguments: bool) -> bool {
if (!has_arguments) {
return true;
}
return false;
}

pub fn result_text_kind(is_object: bool, is_array: bool, is_string: bool) -> str {
if (is_object or is_array) {
return "pretty";
}
if (is_string) {
return "string";
}
return "compact";
}

pub fn call_result_is_error(dispatch_ok: bool) -> bool {
if (!dispatch_ok) {
return true;
}
return false;
}

pub fn error_text_prefix() -> str {
return "Error: ";
}

pub fn content_block_count() -> usize {
return 1;
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original. No test calls the call_tool transport.
// ---------------------------------------------------------------------------

test prop_type_carries_the_named_type {
const p = prop_type("string");
assert(p.type_name == "string");
assert(p.items_type_name == "");
assert(!p.is_array);
}

test prop_array_items_is_an_array_of_strings {
const a = prop_array_items();
assert(a.type_name == "array");
assert(a.items_type_name == "string");
assert(a.is_array);
}

test tool_catalog_nineteen_names {
assert(build_tool_definitions() == 19);
assert(tool_name(0) == "git_status");
assert(tool_name(8) == "git_checkout");
assert(tool_name(11) == "gb_workspace_status");
assert(tool_name(12) == "fs_read_file");
assert(tool_name(18) == "kg_traverse");
assert(tool_name(19) == "");
}

test git_status_tool_facts {
assert(git_status_description() == "List all changed files in a repository");
assert(git_status_required_count() == 1);
assert(git_status_property_count() == 1);
}

test git_stage_files_reads_the_array_descriptor {
assert(git_stage_files_required_count() == 2);
assert(git_stage_files_has_array_property());
}

test git_log_limit_defaults_to_ten {
assert(git_log_limit_property_type() == "integer");
assert(git_log_limit_default() == 10);
}

test service_lists_the_whole_catalog {
const view = McpService_list_tools(McpService_new());
assert(view.tool_count == 19);
assert(!view.meta_present);
assert(!view.next_cursor_present);
}

test call_tool_argument_and_text_kinds {
assert(arguments_become_null(false));
assert(!arguments_become_null(true));
assert(result_text_kind(true, false, false) == "pretty");
assert(result_text_kind(false, true, false) == "pretty");
assert(result_text_kind(false, false, true) == "string");
assert(result_text_kind(false, false, false) == "compact");
}

test call_tool_error_wrapping {
assert(!call_result_is_error(true));
assert(call_result_is_error(false));
assert(error_text_prefix() == "Error: ");
assert(content_block_count() == 1);
}
Loading