>_STRYKE-MCPD
MCP servers without a runtime. Every MCP server in the wild drags a Node runtime or a Python venv to the target machine. A stryke server is a few lines of .stk — and s build --release turns it into a single static native binary with no interpreter on the target. stryke-mcpd is the policy layer over the core mcp_* builtins: validated tool specs with named diagnostics (Mcpd::Schema), crash-isolated serving that keeps stdout protocol-clean (Mcpd::Server), a root-jailed filesystem/shell tool pack (Mcpd::Tools), and result-envelope helpers (Mcpd::Client). Pure stryke: no [ffi] table, no cdylib, no helper binary.
The 4 sublibraries
| # | Module | File | Fns | Highlights |
|---|---|---|---|---|
| 1 | Mcpd::Schema | lib/Schema.stk | 21 | tool (validated spec constructor) · check_args (declared-type arg checking) · validate_all (full specs, unique names) · is_valid (non-dying boolean form) · types · param_names · param_count · has_param · param_type · params_of_type · tool_names (sorted names of a local set) · find (named-spec lookup) · to_json_schema (MCP inputSchema) · from_json_schema (inputSchema → params, inverse) · to_tool_list (MCP tools/list payload) · from_tool_list (tools/list → descriptors, inverse) · tool_map (name → spec dispatch index) · tool_summary · summaries (per-tool list) · describe (catalog) · rename |
| 2 | Mcpd::Server | lib/Server.stk | 9 | serve (validate + wrap + mcp_server_start) · wrap (die → ERROR: envelope, server survives) · wrap_all · checked (arg-check before body) · wrap_all_checked (strict serving) · audit (log successful calls) · log_to/log/log_path (file-only diagnostics) |
| 3 | Mcpd::Tools | lib/Tools.stk | 35 | fs_read · fs_list · fs_glob (shell-glob filter) · fs_count (entry count) · fs_grep · fs_grep_count (match count) · fs_first_match (first hit + line no.) · fs_find (recursive, capped) · fs_stat · fs_readlink (symlink target) · fs_realpath (jailed path resolution) · fs_du · fs_head · fs_tail · fs_slice (line range) · fs_hash (checksum) · fs_exists · fs_lines (wc) · fs_write · fs_append · fs_replace (regex s///) · fs_chmod (octal mode) · fs_mkdir · fs_touch (empty file / mtime) · fs_rmdir · fs_delete · fs_copy · fs_move · sh_exec (allowlist) · env_get · time_now · sys_info · all (readonly mode) · jail (root confinement) · names |
| 4 | Mcpd::Client | lib/Client.stk | 16 | text (envelope → string) · texts (unjoined blocks) · text_lines (split into lines) · content_types · content_of_type (full blocks of a type) · block_count · call_text (call + extract) · call_lines (call + line split) · call_texts (call + unjoined blocks) · is_error · error_message (strip prefix) · tool_names · tool_count · tool_descriptions (name → desc) · has_tool · tool_schema (a tool's advertised inputSchema) |
Color legend: green = module name · yellow = function count. Tools are root-jailed: paths resolve under the configured root and escapes die — which Server::wrap turns into an ERROR result, not a dead server.
Reference — Mcpd::Schema
Validated tool specs. mcp_server_start accepts any +{name, description, parameters, run} hashref and errors one tool at a time at serve time; Schema front-loads that into construction time with named diagnostics, so a malformed spec dies where it was written, not where it was served. A spec is the hashref tool returns; a set is an arrayref of specs.
| Function | Signature | Returns / behavior |
|---|---|---|
| tool | tool($name, $desc, $params, $run) | Validated spec hashref. Dies with the field name if $name is not an identifier (/^[A-Za-z_][A-Za-z0-9_]*$/), $desc is empty, $params is not a hashref, a param type is not one of types, or $run is not a coderef. |
| types | types() | The five wire param types: string number boolean array object. |
| check_args | check_args($spec, $args) | Returns 1, or dies: every declared param must be present, no undeclared extras, and number/array/object are type-checked. Designed for the top of a run coderef. |
| validate_all | validate_all($tools) | Returns the tool count, or dies: $tools must be an arrayref of full specs (all four keys) with unique names. |
| is_valid | is_valid($tools) | 1/0 — non-dying boolean form of validate_all (wraps it in eval). |
| param_names | param_names($spec) | Sorted arrayref of the spec's parameter names. |
| param_count | param_count($spec) | Scalar count of declared parameters — the scalar companion of param_names. |
| has_param | has_param($spec, $name) | 1/0 — whether the spec declares a parameter named $name. |
| param_type | param_type($spec, $name) | The declared wire type of $name, or undef when there is no such parameter. |
| params_of_type | params_of_type($spec, $type) | Sorted arrayref of the parameter names of a given wire type; empty when none match. |
| tool_names | tool_names($tools) | Sorted arrayref of the names in a local set (validates first) — the offline companion of Client::tool_names. |
| find | find($tools, $name) | The spec named $name from a set, or undef (validates first) — single-lookup companion of tool_map. |
| to_json_schema | to_json_schema($spec) | The spec's params as an MCP inputSchema: +{ type => "object", properties => {...}, required => [...] }. Every declared param is required. |
| from_json_schema | from_json_schema($schema) | Inverse of to_json_schema: an inputSchema back into a parameters hashref. Dies if not an object schema or a property has no type. |
| to_tool_list | to_tool_list($tools) | The MCP tools/list payload: arrayref of {name, description, inputSchema} descriptors (validates first). |
| from_tool_list | from_tool_list($list) | Inverse of to_tool_list: descriptors back into {name, description, parameters} hashrefs. The wire format carries no executable, so there is no run. |
| tool_map | tool_map($tools) | A {name => spec} index for O(1) dispatch of a tools/call by name (validates first). |
| tool_summary | tool_summary($spec) | One-line signature: name(a:number, b:string) - description. |
| summaries | summaries($tools) | Arrayref of one tool_summary per tool, roster order (validates first) — array form of describe. |
| describe | describe($tools) | The whole set as a catalog — one tool_summary per line, newline-joined (validates first). |
| rename | rename($spec, $new_name) | A copy of the spec under a new name, re-validated; description / parameters / run preserved. |
Reference — Mcpd::Server
Validated serving with crash isolation. Two rules of stdio MCP servers, both enforced here: stdout belongs to the JSON-RPC protocol (diagnostics go to a log file), and a dying tool must not kill the server (every run coderef is wrapped so a die returns an ERROR: result instead of an EOF).
| Function | Signature | Returns / behavior |
|---|---|---|
| serve | serve($name, $tools) | Validate every spec, wrap_all, then mcp_server_start the stdio transport. Blocks for the life of the server — call it last. |
| wrap | wrap($name, $run) | A new coderef wrapping $run: a die inside the tool returns "ERROR: <message>" to the client (the at FILE line N. location is stripped) and is logged; the server keeps serving. |
| wrap_all | wrap_all($tools) | A copy of the set with every run coderef wrapped; spec metadata preserved. Pure — builds new specs, never serves. |
| checked | checked($spec) | A copy of the spec whose run coderef calls Schema::check_args before the real body — a missing or mistyped arg dies before reaching the body. |
| wrap_all_checked | wrap_all_checked($tools) | A copy of the set with every run coderef arg-checked (checked) and error-wrapped (wrap) — the strict serving variant of wrap_all (validates first). |
| audit | audit($name, $run) | Like wrap but also logs successful calls (tool name + result byte length) — an audit trail. |
| log_to | log_to($path) | Route diagnostics to $path (appended, one line per event). Never logs to stdout. |
| log | log($msg) | Append a timestamped, newline-terminated line to the configured log file. No-op until log_to is called. |
| log_path | log_path() | The configured log path, or undef when log_to was never called. |
Reference — Mcpd::Tools
The root-jailed stock tool pack. Every tool that takes a path resolves it under the configured root via jail; an escape dies, and Server::wrap turns that into an ERROR: path escapes root result rather than a dead server. Each tool is a Schema::tool generator that takes the same $opts hashref (carrying root); call them directly, or assemble the whole pack with all.
| Tool | What it does |
|---|---|
| fs_read | Read a file's contents (jailed path). |
| fs_list | List a directory's entries (jailed path). |
| fs_glob | List entries matching a shell glob (jailed path). |
| fs_count | Count entries in a directory (jailed path). |
| fs_grep | Grep a file for a pattern (jailed path). |
| fs_grep_count | Count matching lines for a pattern (jailed path). |
| fs_first_match | First hit for a pattern plus its line number (jailed path). |
| fs_find | Recursive file find, depth-capped (jailed path). |
| fs_stat | File metadata (jailed path). |
| fs_readlink | The target of a symlink (jailed path). |
| fs_realpath | Resolve a path within the jail. |
| fs_du | Recursive size of a path (jailed path). |
| fs_head | First N lines of a file (jailed path). |
| fs_tail | Last N lines of a file (jailed path). |
| fs_slice | A line range of a file (jailed path). |
| fs_hash | Checksum of a file (jailed path). |
| fs_exists | Whether a path exists (jailed path). |
| fs_lines | Line count of a file (jailed path). |
| fs_write | Write content to a file (jailed path). Mutating. |
| fs_append | Append content to a file (jailed path). Mutating. |
| fs_replace | Regex s/// over a file (jailed path). Mutating. |
| fs_chmod | Set an octal file mode (jailed path). Mutating. |
| fs_mkdir | Create a directory (jailed path). Mutating. |
| fs_touch | Create an empty file / bump mtime (jailed path). Mutating. |
| fs_rmdir | Remove a directory (jailed path). Mutating. |
| fs_delete | Delete a file (jailed path). Mutating. |
| fs_copy | Copy a file (jailed paths). Mutating. |
| fs_move | Move/rename a file (jailed paths). Mutating. |
| sh_exec | Run a shell command with an optional first-word allowlist. Mutating. |
| env_get | Read an environment variable. |
| time_now | The current time. |
| sys_info | System information. |
| all | The whole pack as one arrayref. all(+{root => ..., readonly => 1}) drops every mutating tool. |
| jail | jail($root, $path) — the lexical path resolver every fs tool runs input through; escapes die. |
| names | The names of every stock tool this sublib ships (full, non-readonly pack). |
The full pack (all(+{root => ...})) is 32 tool specs; all(+{root => ..., readonly => 1}) is 21 — it omits 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).
Reference — Mcpd::Client
Result-envelope helpers for the client side. mcp_connect, mcp_tools, mcp_call, mcp_close are builtins — none re-wrapped here. What this sublib adds is unpacking the MCP result envelope (+{content => [+{type => "text", text => ...}, ...]}) and the call-then-unpack composites that round-trip tests live on. $h is a connection handle; $result is an mcp_call result.
| Function | Signature | Returns / behavior |
|---|---|---|
| text | text($result) | The result's text blocks joined into one string; "" when there is no text content. |
| texts | texts($result) | Every text block as a list (unjoined), in envelope order; empty list when none. |
| text_lines | text_lines($result) | The joined text split into lines; empty list for an empty result. |
| content_types | content_types($result) | The distinct content-block types present, sorted (e.g. ["image","text"]). |
| content_of_type | content_of_type($result, $type) | All content blocks of $type as full block hashrefs (keeping data/mimeType/resource); the general form of texts. |
| block_count | block_count($result) | The number of content blocks of every type; 0 for an empty result. |
| call_text | call_text($h, $name, $args) | mcp_call + text in one step — the round-trip primitive. |
| call_lines | call_lines($h, $name, $args) | mcp_call + text_lines — for tools whose result is a list of rows. |
| call_texts | call_texts($h, $name, $args) | mcp_call + texts — the multi-block round-trip primitive. |
| is_error | is_error($result) | 1/0 — whether the result is an ERROR: envelope (a tool that died behind Server::wrap). |
| error_message | error_message($result) | The error text without the ERROR: prefix, or undef when not an error envelope. |
| tool_names | tool_names($h) | The tool names a connected server exposes, sorted. |
| tool_count | tool_count($h) | How many tools a connected server exposes — scalar companion of tool_names. |
| tool_descriptions | tool_descriptions($h) | A {name => description} map of every exposed tool; no-description tools map to "". |
| has_tool | has_tool($h, $name) | 1/0 — whether a connected server exposes a tool named $name. |
| tool_schema | tool_schema($h, $name) | The advertised JSON inputSchema of the tool named $name, or undef. |
Configuration
Two knobs drive the stock pack and serving discipline; everything else is the spec you pass to serve.
| Setting | Where | Effect |
|---|---|---|
root | Mcpd::Tools::all(+{ root => ... }) (and every fs_* generator) | The jail root. Relative tool paths anchor under it; .. resolves but dies the moment it would climb above root; absolute paths outside root die. |
readonly | Mcpd::Tools::all(+{ root => ..., readonly => 1 }) | Drops the 11 mutating tools, leaving 21 read-only tools — for exposing a filesystem to a model without write/exec. |
| log path | Mcpd::Server::log_to($path) | Append-only diagnostics file. Without it, log/audit are no-ops and stdout stays clean by default. |
Examples
Four runnable scripts in examples/, each self-contained:
| File | Shows |
|---|---|
examples/calc_server.stk | A minimal server: add, reverse_text, and always_fails (which dies to demonstrate the error envelope). The exact shape the round-trip test drives. |
examples/round_trip.stk | The client side: mcp_connect to calc_server.stk over stdio, list tools, call add/reverse_text, call the dying tool, and prove the server survived. |
examples/stock_server.stk | The stock pack jailed to the current directory, served on stdio — point any MCP client at it with { "command": "stryke", "args": ["examples/stock_server.stk"] }. |
examples/jailed_fs.stk | A live stdio round trip against the stock pack jailed to a temp dir: write a file, read it back, list it, then watch a ../../etc/passwd escape come back as a blocked error envelope. |
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Client sees a corrupted JSON-RPC stream | Something wrote to stdout. In this package nothing in lib/ prints to stdout (CI pins it); route your own diagnostics through Mcpd::Server::log_to, never p/print to stdout in a served tool. |
A tool returns ERROR: path escapes root | The requested path resolved outside the jail root. This is the jail working as designed; the server stays up. Pass a path under root, or widen root. |
A tool returns ERROR: ... instead of a value | The tool died; Server::wrap converted it to a result so the connection survives. Use Mcpd::Client::error_message to read the message; check the server log if log_to is configured. |
| A malformed spec dies at startup | Schema::tool/validate_all front-load validation. Read the named diagnostic (it carries the offending tool and field) and fix the spec where it was written. |
| Diagnostics aren't being written | log/audit are no-ops until Mcpd::Server::log_to($path) is called. Set it before serve. |
Install
# From a release: s pkg install -g github.com/MenkeTechnologies/stryke-mcpd # From a local checkout: git clone https://github.com/MenkeTechnologies/stryke-mcpd cd stryke-mcpd s pkg install -g . # installs into ~/.stryke/store/stryke-mcpd@<version>/ # Or via Makefile: make install
No cargo step. No cdylib. The installed store directory contains only stryke.toml + lib/*.stk — no compiled artifacts, fully portable across platforms.
Quick Start
use Mcpd
# A server is one call: validated specs in, stdio JSON-RPC out.
Mcpd::Server::log_to("$ENV{HOME}/.cache/calc.log") # stdout stays clean
Mcpd::Server::serve("calc", [
Mcpd::Schema::tool("add", "Add two numbers",
+{ a => "number", b => "number" },
fn { val $args = shift; $args->{a} + $args->{b} }),
@{ Mcpd::Tools::all(+{ root => getcwd() }) }, # stock pack, jailed
])
# Client side — round trip any server over stdio:
val $h = mcp_connect("stdio:stryke calc_server.stk")
p join(", ", @{ Mcpd::Client::tool_names($h) })
p Mcpd::Client::call_text($h, "add", +{ a => 2, b => 40 }) # 42
p Mcpd::Client::is_error(mcp_call($h, "always_fails", +{})) # 1 — server still alive
mcp_close($h)
# Ship it: AOT-compile the server to a single static native binary.
# s build --release # target/release/<name> — no interpreter on target
Pull only what you need with explicit sub-imports: use Mcpd::Schema, use Mcpd::Client, etc. The umbrella use Mcpd is just for convenience.
What's NOT in Here
By design — these are stryke builtins, so we don't re-wrap them. If a function in this library can be replaced with one builtin call, it's a bug.
| Category | Builtins (call directly) |
|---|---|
| Server plumbing | mcp_server_start · mcp_serve_registered_tools · ai_register_tool and the tool fn / mcp_server "name" { ... } source-level desugar |
| Client plumbing | mcp_connect · mcp_tools · mcp_resources · mcp_prompts · mcp_call · mcp_resource · mcp_prompt · mcp_close |
| AI attachment | mcp_attach_to_ai · mcp_detach_from_ai · mcp_attached |
| File / process | slurp · spurt · getcwd · qx(...) · opendir/readdir |
CLI
s bin/mcpd.stk new myserver # write ./myserver.stk skeleton (validated, logged, ready) s bin/mcpd.stk serve-stock /srv/data # stock pack jailed to /srv/data, served on stdio s bin/mcpd.stk tools # list the stock tool pack s bin/mcpd.stk version s bin/mcpd.stk help
Wire a server into any MCP client config as { "command": "stryke", "args": ["server.stk"] } — or build it and point at the binary with no args at all.
Tests
s test t/ # assertions across every public function
t/test_mcpd.stk asserts Schema/Tools/Client as pure functions (spec validation, arg checking, jail escapes, envelope parsing), then runs a live round trip: generate a server script, serve it over stdio via mcp_connect, list tools, call one, kill one, prove the server survived. Headless-CI safe. Exits TAP-style via test_run.
Layout
stryke-mcpd/ ├── stryke.toml # pure-stryke package manifest (no [ffi]) ├── Makefile # test / install / clean ├── LICENSE # MIT ├── lib/ │ ├── Mcpd.stk # `use Mcpd` — pulls all four sublibs │ ├── Schema.stk # `use Mcpd::Schema` — validated tool specs │ ├── Server.stk # `use Mcpd::Server` — crash-isolated serving │ ├── Tools.stk # `use Mcpd::Tools` — root-jailed stock pack │ └── Client.stk # `use Mcpd::Client` — result-envelope helpers ├── bin/ │ └── mcpd.stk # CLI front-end (new / serve-stock / tools) ├── t/ │ └── test_mcpd.stk # pure asserts + live stdio round trip ├── examples/ │ ├── calc_server.stk # minimal server (the round-trip target shape) │ ├── round_trip.stk # client: list, call, error envelope, survival │ ├── stock_server.stk # stock pack jailed to cwd │ └── jailed_fs.stk # live write/read + a blocked jail escape over stdio ├── tests/ # shell gate scripts (CI lints) └── docs/ # this site (GitHub Pages)
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging