diff --git a/specs/port/trios/crates/trios-server/src/mcp.t27 b/specs/port/trios/crates/trios-server/src/mcp.t27 new file mode 100644 index 0000000000..423eb962f2 --- /dev/null +++ b/specs/port/trios/crates/trios-server/src/mcp.t27 @@ -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": }; `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`. 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); +}