>_EXECUTIVE SUMMARY
stryke-mcpd turns stryke's core MCP plumbing into shippable servers. The artifact story is the point: every mainstream MCP server requires a Node runtime or Python venv on the target machine; a stryke server is a script during development and a single static native binary after s build --release. The package contributes the two disciplines stdio servers live or die by — a protocol-clean stdout and crash isolation per tool — plus the parts every server rewrites: spec validation, arg checking, a root-jailed stock tool pack, and client-side envelope extraction for round-trip tests.
Boundary rule, same as every pure-stryke package: protocol mechanism (JSON-RPC framing, transport, tool registry) is strykelang core (mcp.rs, ai_sugar.rs); policy (what a valid spec is, what happens when a tool dies, where diagnostics go) lives here and versions independently.
~SUBLIBRARY BOUNDARIES
| Sublib | Owns | Doesn't own |
|---|---|---|
Mcpd::Schema | Construction-time validation: identifier names, non-empty descriptions, wire-type params (string/number/boolean/array/object), coderef runs; check_args for declared-type arg checking inside a tool; validate_all for whole-set checks (full specs, unique names); introspection (param_names, tool_summary, describe) and rename. | Serving. Schema builds and checks hashrefs; it never touches a transport. |
Mcpd::Server | The two disciplines: file-only logging (log_to/log/log_path — stdout carries JSON-RPC) and crash isolation (wrap converts a tool's die into an ERROR: text result — with the stryke die location stripped so the client sees only the message — and the server keeps serving; wrap_all applies it across a set; audit adds a success log). serve = validate + wrap_all + mcp_server_start. | Protocol framing, transport, request dispatch — all mcp_server_start (core). |
Mcpd::Tools | The stock pack: fs_read/fs_list/fs_glob/fs_count/fs_grep/fs_grep_count/fs_first_match/fs_find/fs_stat/fs_readlink/fs_realpath/fs_du/fs_head/fs_tail/fs_slice/fs_hash/fs_exists/fs_lines/fs_write/fs_append/fs_replace/fs_chmod/fs_mkdir jailed under a configurable root (lexical .. resolution, escapes die), sh_exec with optional command allowlist, env_get/time_now/sys_info. all returns the pack as one list (32 specs); all(+{readonly => 1}) drops the 11 mutating tools (fs_write, fs_append, fs_replace, fs_chmod, fs_mkdir, fs_touch, fs_rmdir, fs_delete, fs_copy, fs_move, sh_exec), leaving 21. | Anything not generally useful. Domain tools belong in the consumer's server script, composed alongside the stock pack. |
Mcpd::Client | Envelope policy: text joins text blocks from +{content => [...]}, texts returns them unjoined, content_types lists distinct block types, call_text is the call-then-extract composite, is_error/error_message recognize and unwrap the Server envelope, tool_names/has_tool query the discovery list. | Connection plumbing — mcp_connect/mcp_tools/mcp_call/mcp_close are builtins, never re-wrapped. |
=PUBLIC SURFACE
81 public functions across four sublibs, plus the bin/mcpd.stk CLI front-end. Every function is a fn Mcpd::<Sublib>::<name> in lib/; counts are the top-level definitions in each file.
| Sublib | File | Fns | Surface |
|---|---|---|---|
Mcpd::Schema | lib/Schema.stk | 21 | Spec builder + validation (tool, check_args, validate_all, is_valid), introspection (types, param_names, param_count, has_param, param_type, params_of_type, tool_names, find), wire-format conversion (to_json_schema/from_json_schema, to_tool_list/from_tool_list, tool_map), catalogs (tool_summary, summaries, describe), rename. |
Mcpd::Server | lib/Server.stk | 9 | serve, wrap, wrap_all, checked, wrap_all_checked, audit, log_to, log, log_path. |
Mcpd::Tools | lib/Tools.stk | 35 | 32 stock tool generators (18 read + 11 mutating + env_get/time_now/sys_info — 32 specs in the full served pack, 21 read-only) plus all, jail, names. |
Mcpd::Client | lib/Client.stk | 16 | Envelope unpacking (text, texts, text_lines, content_types, content_of_type, block_count), call composites (call_text, call_lines, call_texts), error handling (is_error, error_message), discovery (tool_names, tool_count, tool_descriptions, has_tool, tool_schema). |
The bin/mcpd.stk CLI front-end is pure stryke (no helper binary): new NAME [DIR] writes a validated server skeleton, serve-stock [ROOT] serves the stock pack jailed to ROOT (default getcwd()) over stdio, tools lists the stock tool names, version prints the package version, help prints usage. Wire a built or scripted server into any MCP client as { "command": "stryke", "args": ["server.stk"] }.
$THE TWO STDIO DISCIPLINES
| Discipline | Mechanism | Enforcement |
|---|---|---|
| stdout is protocol | Mcpd::Server::log_to(PATH) routes every diagnostic to an append-only file; Mcpd::Server::log is a no-op until configured. Nothing in lib/ writes to stdout. | tests/repo-contract.sh greps lib/*.stk for p /print /say (file-handle prints excepted) and fails CI on any hit. |
| a dying tool is a result, not an outage | Mcpd::Server::wrap evals every run coderef: die → logged + returned as ERROR: <msg> text. The client sees a result; the connection stays up. | Round-trip assertion in t/: call kaboom (dies), assert error envelope, then call add on the same connection and assert 42/2. |
&THE ROOT JAIL
Stock filesystem tools are exposed to a language model on the other end of the wire; unbounded paths are how an fs_read becomes an exfiltration primitive. Mcpd::Tools::jail($root, $path) is a lexical resolver: relative paths anchor under root, ./// collapse, .. resolves but dies the moment it would climb above root, and absolute paths outside root die. The die propagates into Server::wrap's envelope — the client gets ERROR: path escapes root, the server keeps serving. sh_exec gets the same treatment via an optional first-word allowlist.
/WHY A PACKAGE, NOT CORE
- Core owns the protocol. JSON-RPC framing, the stdio transport, tool registration, the
tool fndesugar —mcp.rs+ai_sugar.rs, Rust, stable. - The package owns server policy. Validation strictness, error envelope format, log destination, jail semantics, the stock pack roster — opinions that should iterate at package speed, not language-release speed.
- The artifact story needs nothing extra.
s build --releaseis a strykelang capability; mcpd just makes sure what you build is a disciplined server. No cdylib, no per-triple release matrix — the package tarball is five.stkfiles.
#TEST SURFACE
t/test_mcpd.stk uses stryke's native assertions (assert_eq, assert_ok, assert_match, assert_dies) and exits TAP-style via test_run. Headless-CI safe — the round trip serves a generated script on the same machine over stdio.
| Area | Assertion families covered |
|---|---|
| Schema | tool accepts a full spec and rejects non-identifier names, empty descriptions, unknown param types, non-coderef runs; check_args accepts exact args and rejects missing / mistyped / extra params; validate_all counts, rejects non-arrayrefs, duplicates, partial specs; param_names sorted, tool_summary signature, describe catalog + validation, rename preserves fields and re-validates. |
| Server | wrap passes results through and converts die to the ERROR: envelope; log_to + log + log_path manage the configured file; wrap_all preserves every spec key while wrapping (pins strykelang BUG-305); audit logs successful calls with a byte count. |
| Tools | jail resolves inside, collapses interior .., dies on dotdot and absolute escapes; stock pack validates as 32 specs (21 in readonly mode); fs_read/fs_list/fs_glob/fs_count/fs_grep/fs_grep_count/fs_first_match/fs_find/fs_stat/fs_readlink/fs_realpath/fs_write/fs_replace/fs_chmod all exercised through the jail incl. escape rejection, depth caps, dotfile listing; sh_exec runs an allowlisted command and rejects everything else. |
| Client | text joins multi-block content and returns "" on empty; texts unjoined; content_types distinct + sorted; is_error detects the wrap envelope; error_message strips the prefix (undef on clean); has_tool queries discovery over the live round trip. |
| Round trip | Generate server script → mcp_connect("stdio:stryke ...") → tool list matches → add(2,40) returns 42 → dying tool returns the envelope → same connection still serves. |
%CI GATES
22 shell gate scripts in tests/ are wired into .github/workflows/ci.yml, plus a stryke job that installs the latest strykelang release binary, syntax-checks every .stk file, installs the package into the global store, and runs s test t/. Every gate is reproducible locally with bash tests/<gate>.sh.
| Bucket | Gates |
|---|---|
| repo-contract | Pure-stryke (no Cargo.toml, no [ffi]). lib/ has exactly 5 files. Every sublib declares package Mcpd::<Name>. stdout discipline: lib/ never prints to stdout. Examples shebang. bin/mcpd.stk shebang. Makefile has test/install/clean. LICENSE is MIT. |
| docs gates | <h1> present, <body> tag, closing </html>, no deprecated tags, no inline event handlers, no placeholder hrefs, no plaintext http://, target="_blank" always paired with rel="noopener noreferrer". |
| README gates | At least one shields.io badge, at least one h2 section, at least one https link, ends with final newline. |
| polish gates | Workflow files have no tabs (YAML), every tests/*.sh has +x and a bash shebang, man pages (if any) have synopsis section + final newline + no trailing whitespace. |
!EXTENDING
Adding a stock tool means three edits:
- Add the
fn Mcpd::Tools::namegenerator tolib/Tools.stkreturning aMcpd::Schema::tool(...)spec; jail any path input, allowlist any exec input. Add it toMcpd::Tools::namesandall. - Assert it in
t/test_mcpd.stk: spec validates, happy path runs through$spec->{run}->(...)directly, every guard dies. - Mention it in the README sublib table and
docs/index.html.
Custom servers don't touch this repo: mcpd new NAME emits a skeleton that pulls the package from the store.
@PROJECT METADATA
| Item | Value |
|---|---|
| License | MIT |
| Author | MenkeTechnologies |
| Repository | github.com/MenkeTechnologies/stryke-mcpd |
| Parent language | strykelang |
| Meta umbrella | MenkeTechnologiesMeta |
| Issues | github.com/MenkeTechnologies/stryke-mcpd/issues |