// STRYKE-MCPD — MCP SERVERS, NATIVE

4 sublibraries · validated tool specs · crash-isolated serving · root-jailed stock pack · s build --release → one native binary · use Mcpd pulls everything

Report GitHub Issues
// Color scheme

>_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

#ModuleFileFnsHighlights
1Mcpd::Schemalib/Schema.stk21tool (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
2Mcpd::Serverlib/Server.stk9serve (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)
3Mcpd::Toolslib/Tools.stk35fs_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
4Mcpd::Clientlib/Client.stk16text (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.

FunctionSignatureReturns / behavior
tooltool($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.
typestypes()The five wire param types: string number boolean array object.
check_argscheck_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_allvalidate_all($tools)Returns the tool count, or dies: $tools must be an arrayref of full specs (all four keys) with unique names.
is_validis_valid($tools)1/0 — non-dying boolean form of validate_all (wraps it in eval).
param_namesparam_names($spec)Sorted arrayref of the spec's parameter names.
param_countparam_count($spec)Scalar count of declared parameters — the scalar companion of param_names.
has_paramhas_param($spec, $name)1/0 — whether the spec declares a parameter named $name.
param_typeparam_type($spec, $name)The declared wire type of $name, or undef when there is no such parameter.
params_of_typeparams_of_type($spec, $type)Sorted arrayref of the parameter names of a given wire type; empty when none match.
tool_namestool_names($tools)Sorted arrayref of the names in a local set (validates first) — the offline companion of Client::tool_names.
findfind($tools, $name)The spec named $name from a set, or undef (validates first) — single-lookup companion of tool_map.
to_json_schemato_json_schema($spec)The spec's params as an MCP inputSchema: +{ type => "object", properties => {...}, required => [...] }. Every declared param is required.
from_json_schemafrom_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_listto_tool_list($tools)The MCP tools/list payload: arrayref of {name, description, inputSchema} descriptors (validates first).
from_tool_listfrom_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_maptool_map($tools)A {name => spec} index for O(1) dispatch of a tools/call by name (validates first).
tool_summarytool_summary($spec)One-line signature: name(a:number, b:string) - description.
summariessummaries($tools)Arrayref of one tool_summary per tool, roster order (validates first) — array form of describe.
describedescribe($tools)The whole set as a catalog — one tool_summary per line, newline-joined (validates first).
renamerename($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).

FunctionSignatureReturns / behavior
serveserve($name, $tools)Validate every spec, wrap_all, then mcp_server_start the stdio transport. Blocks for the life of the server — call it last.
wrapwrap($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_allwrap_all($tools)A copy of the set with every run coderef wrapped; spec metadata preserved. Pure — builds new specs, never serves.
checkedchecked($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_checkedwrap_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).
auditaudit($name, $run)Like wrap but also logs successful calls (tool name + result byte length) — an audit trail.
log_tolog_to($path)Route diagnostics to $path (appended, one line per event). Never logs to stdout.
loglog($msg)Append a timestamped, newline-terminated line to the configured log file. No-op until log_to is called.
log_pathlog_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.

ToolWhat it does
fs_readRead a file's contents (jailed path).
fs_listList a directory's entries (jailed path).
fs_globList entries matching a shell glob (jailed path).
fs_countCount entries in a directory (jailed path).
fs_grepGrep a file for a pattern (jailed path).
fs_grep_countCount matching lines for a pattern (jailed path).
fs_first_matchFirst hit for a pattern plus its line number (jailed path).
fs_findRecursive file find, depth-capped (jailed path).
fs_statFile metadata (jailed path).
fs_readlinkThe target of a symlink (jailed path).
fs_realpathResolve a path within the jail.
fs_duRecursive size of a path (jailed path).
fs_headFirst N lines of a file (jailed path).
fs_tailLast N lines of a file (jailed path).
fs_sliceA line range of a file (jailed path).
fs_hashChecksum of a file (jailed path).
fs_existsWhether a path exists (jailed path).
fs_linesLine count of a file (jailed path).
fs_writeWrite content to a file (jailed path). Mutating.
fs_appendAppend content to a file (jailed path). Mutating.
fs_replaceRegex s/// over a file (jailed path). Mutating.
fs_chmodSet an octal file mode (jailed path). Mutating.
fs_mkdirCreate a directory (jailed path). Mutating.
fs_touchCreate an empty file / bump mtime (jailed path). Mutating.
fs_rmdirRemove a directory (jailed path). Mutating.
fs_deleteDelete a file (jailed path). Mutating.
fs_copyCopy a file (jailed paths). Mutating.
fs_moveMove/rename a file (jailed paths). Mutating.
sh_execRun a shell command with an optional first-word allowlist. Mutating.
env_getRead an environment variable.
time_nowThe current time.
sys_infoSystem information.
allThe whole pack as one arrayref. all(+{root => ..., readonly => 1}) drops every mutating tool.
jailjail($root, $path) — the lexical path resolver every fs tool runs input through; escapes die.
namesThe 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.

FunctionSignatureReturns / behavior
texttext($result)The result's text blocks joined into one string; "" when there is no text content.
textstexts($result)Every text block as a list (unjoined), in envelope order; empty list when none.
text_linestext_lines($result)The joined text split into lines; empty list for an empty result.
content_typescontent_types($result)The distinct content-block types present, sorted (e.g. ["image","text"]).
content_of_typecontent_of_type($result, $type)All content blocks of $type as full block hashrefs (keeping data/mimeType/resource); the general form of texts.
block_countblock_count($result)The number of content blocks of every type; 0 for an empty result.
call_textcall_text($h, $name, $args)mcp_call + text in one step — the round-trip primitive.
call_linescall_lines($h, $name, $args)mcp_call + text_lines — for tools whose result is a list of rows.
call_textscall_texts($h, $name, $args)mcp_call + texts — the multi-block round-trip primitive.
is_erroris_error($result)1/0 — whether the result is an ERROR: envelope (a tool that died behind Server::wrap).
error_messageerror_message($result)The error text without the ERROR: prefix, or undef when not an error envelope.
tool_namestool_names($h)The tool names a connected server exposes, sorted.
tool_counttool_count($h)How many tools a connected server exposes — scalar companion of tool_names.
tool_descriptionstool_descriptions($h)A {name => description} map of every exposed tool; no-description tools map to "".
has_toolhas_tool($h, $name)1/0 — whether a connected server exposes a tool named $name.
tool_schematool_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.

SettingWhereEffect
rootMcpd::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.
readonlyMcpd::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 pathMcpd::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:

FileShows
examples/calc_server.stkA 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.stkThe 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.stkThe 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.stkA 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

SymptomCause / fix
Client sees a corrupted JSON-RPC streamSomething 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 rootThe 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 valueThe 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 startupSchema::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 writtenlog/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.

CategoryBuiltins (call directly)
Server plumbingmcp_server_start · mcp_serve_registered_tools · ai_register_tool and the tool fn / mcp_server "name" { ... } source-level desugar
Client plumbingmcp_connect · mcp_tools · mcp_resources · mcp_prompts · mcp_call · mcp_resource · mcp_prompt · mcp_close
AI attachmentmcp_attach_to_ai · mcp_detach_from_ai · mcp_attached
File / processslurp · 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: