skiff
Turn an MCP server, OpenAPI spec, or GraphQL endpoint into a CLI at runtime, with no codegen. Aimed at agent workflows: progressive discovery, a warm catalog index, sessions, and spool for large payloads.
Inspired by knowsuchagency/mcp2cli (Python prior art). skiff is a separate Rust project, focused on warm-path latency and lighter agent defaults.
Install
# Homebrew (samikh-git tap)
# crates.io (package name skiff-cli; binary is skiff)
# from source
# or: cargo build --release && cargo install --path .
# Agent skill (Cursor / Claude / Codex / … via skills CLI)
# or: npx skills add samikh-git/skiff -g # global
# npx skills add samikh-git/skiff -s skiff -y # non-interactive
Binary: skiff (or ./target/release/skiff).
Quick start
One source flag is required (--spec | --mcp | --mcp-stdio | --graphql).
# OpenAPI
# MCP stdio
# MCP HTTP (auto → streamable, then SSE)
# GraphQL
Sessions (Unix only)
Runs one long-lived MCP client behind a Unix-domain socket so agents do not pay npx/initialize on every call.
| Detail | Behavior |
|---|---|
| Layout | $SKIFF_CACHE_DIR/sessions/{name}.{sock,json,log} |
| Security | Socket 0o600, same-UID peer check; start config is a 0o600 file (not argv), unlinked after read |
| Idle | Default 30m (--session-idle-secs / SKIFF_SESSION_IDLE_SECS; 0 = never) |
| Bake | bake create … --session NAME so @name reuses the warm daemon |
| Failure | If the MCP child dies, IPC errors; --session-stop then --session-start again |
Not available on Windows.
OAuth
HTTP sources only (not --mcp-stdio). Prefer streamable HTTP; SSE gets a Bearer at connect only (no mid-stream refresh).
Tokens: $SKIFF_CACHE_DIR/oauth/ (default ~/.cache/skiff/oauth/).
Bake / @name
Configs: $SKIFF_CONFIG_DIR/baked.json (default ~/.config/skiff/baked.json).
Security
- Secrets: use
env:/file:prefixes on--auth-headerand OAuth flags. Never put literal tokens on argv. - Trust: treat remote MCP/API responses as untrusted input.
- URLs: remote
--spec/--mcp/--graphqlfetches are not SSRF-sandboxed; only pass URLs you trust. - Cache dir: do not point
SKIFF_CACHE_DIRat a shared world-writable path (sessions and OAuth store secrets there). - Sessions: same-UID local IPC only;
--session-clean-envfor untrusted stdio servers.
Agent skill
The agent skill is in skills/skiff/, which npx skills discovers. Install it with:
In this repo, .cursor/skills/skiff is a symlink to that directory for Cursor.
For agents, start with --agent (or SKIFF_AGENT=1), then use --detail names|brief, --describe / TOOL --help --json, and --json or --toon. Oversized results go to $SKIFF_CACHE_DIR/spool/ with a short stdout pointer you can rg.
Cloudflare MCP byte bench (optional):
CF_API_TOKEN=… SKIFF_BENCH_CF=1
Rust vs Python multi-run comparison (dataframe + optional CSV; needs pandas and uvx):
# SKIFF_BIN=… SKIFF_PYTHON_BIN="uvx --with mcp==1.12.0 mcp2cli" \
# python3 scripts/bench_vs_python.py --csv /tmp/bench.csv
Benchmark results (Cloudflare MCP, 10 warm runs)
Measured 2026-07-31 against upstream Python mcp2cli via uvx --with mcp==1.12.0 mcp2cli (plain uvx mcp2cli currently fails streamable HTTP on newer mcp SDKs). Isolated SKIFF_CACHE_DIR per implementation. Wall clock includes process spawn.
| Scenario | Rust warm median | Python warm median | Approx. speedup |
|---|---|---|---|
Docs MCP --list --json --compact |
8.2 ms | 575 ms | ~70× |
Docs MCP --list --json |
12.8 ms | 555 ms | ~43× |
Fat catalog --search workers --json --compact --top 20 |
9.9 ms | 3224 ms | ~325× |
Fat catalog --list --json --compact |
12.8 ms | 1846 ms | ~144× |
Fat catalog session --agent --search workers (Rust-only) |
12.8 ms | - | - |
Cold first fetch of the fat Cloudflare catalog is ~1-2 s for Rust and ~2-3.5 s for Python (network + full list_tools). After that, Rust stays near 10 ms because warm discovery reads a slim v4 tools index (~143 KiB names + overrides; postings rebuilt in memory) or, with --session, searches an in-daemon RAM index over Unix IPC. Python's warm path still pays a large per-invocation cost on this catalog (often seconds), so spawn and cache alone do not explain the gap.
Index / session notes
- Non-session disk index omits descriptions and postings by default;
--detail briefmay fall through to the full tools cache. - Session daemons hold
CompactIndexin RAM and answerlist_tools_lightwithout shippinginputSchemas. Prefer this for fat servers. - Full
tools.jsonremains ~2.4 MB for schema-heavy calls (--detail full,--describe, tool--help).
Limitations of this comparison
- Not identical stdout: on fat
--search … --top 20, Python returned fewer bytes (~719 vs ~1100). Formats and hit sets can differ; do not treat byte counts as a pure efficiency win either way. - Heuristic tokens:
ceil(bytes/4), not tiktoken. - Auth / transport: both use
--transport streamableandAuthorization:Bearer …. Results depend on Cloudflare MCP availability and token scope. - Python pin: SDK pin is required for a fair streamable run; future upstream fixes may change absolute Python numbers.
- Machine noise: medians over 10 warm runs on one laptop; expect variance across OS/load.
- Feature asymmetry:
--agent, sessions, spool, and native--toonare Rust-only in this harness.
The clearest gap is warm discovery latency, especially search over thousands of tools, which is what the compact index and session RAM path are for. Token and byte counts still depend on the scenario: progressive --detail / --top / --agent matter more for context size than raw CLI speed.
Status / limits
Shipped: OpenAPI, MCP stdio/HTTP (streamable + SSE), OAuth, GraphQL, sessions (Unix), bake/@name, list/search/output flags, native --toon, --envelope, spool overflow, --agent defaults.
Not done yet: no Windows sessions; no mid-daemon OAuth refresh (restart the session if the token TTL is shorter than idle).
License
MIT. See LICENSE. Python mcp2cli © Stephan Fitzpatrick / knowsuchagency.