skiff-cli 0.1.2

Progressive MCP / OpenAPI / GraphQL CLI for agents — fast warm discovery, sessions, spool
Documentation

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)
brew tap samikh-git/tools
brew install skiff

# crates.io (package name skiff-cli; binary is skiff)
cargo install skiff-cli

# from source
cargo install --git https://github.com/samikh-git/skiff --locked
# or: cargo build --release && cargo install --path .

# Agent skill (Cursor / Claude / Codex / … via skills CLI)
npx skills add samikh-git/skiff
# 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
skiff --spec ./tests/fixtures/petstore.json --base-url http://127.0.0.1:8080/api/v1 --list
skiff --spec ./openapi.json --base-url https://api.example.com list-pets --limit 5

# MCP stdio
skiff --mcp-stdio "python3 ./tests/fixtures/mcp_test_server.py" --list
skiff --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp" \
  read-file --path /tmp/hello.txt

# MCP HTTP (auto → streamable, then SSE)
skiff --mcp http://127.0.0.1:8000/mcp --list
skiff --mcp http://127.0.0.1:8000/sse --transport sse echo --message hi

# GraphQL
skiff --graphql http://127.0.0.1:4000 --list
skiff --graphql http://127.0.0.1:4000 --fields "id name" user --id 1

Sessions (Unix only)

Runs one long-lived MCP client behind a Unix-domain socket so agents do not pay npx/initialize on every call.

skiff --mcp-stdio "python3 ./tests/fixtures/mcp_test_server.py" --session-start myfs
skiff --session myfs --list
skiff --session myfs echo --message hi
skiff --session myfs --list-resources
skiff --session-list --json
skiff --session-stop myfs
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).

skiff --mcp https://mcp.example.com/mcp --oauth --list
skiff --mcp https://mcp.example.com/mcp \
  --oauth-client-id env:OAUTH_CLIENT_ID \
  --oauth-client-secret env:OAUTH_CLIENT_SECRET \
  --oauth-flow client_credentials --list
skiff --mcp https://mcp.example.com/mcp --oauth-clear

Tokens: $SKIFF_CACHE_DIR/oauth/ (default ~/.cache/skiff/oauth/).

Bake / @name

skiff bake create petstore --spec ./tests/fixtures/petstore.json --methods GET,POST
skiff bake create mytools \
  --mcp-stdio "python3 ./tests/fixtures/mcp_test_server.py" --exclude deploy --session myfs

skiff @petstore --list
skiff @mytools echo --message hi   # after --session-start myfs

skiff bake list
skiff bake show petstore           # secrets masked
skiff bake update petstore --cache-ttl 7200
skiff bake remove petstore
skiff bake install mytools --dir ./scripts/

Configs: $SKIFF_CONFIG_DIR/baked.json (default ~/.config/skiff/baked.json).

Security

  • Secrets: use env: / file: prefixes on --auth-header and OAuth flags. Never put literal tokens on argv.
  • Trust: treat remote MCP/API responses as untrusted input.
  • URLs: remote --spec / --mcp / --graphql fetches are not SSRF-sandboxed; only pass URLs you trust.
  • Cache dir: do not point SKIFF_CACHE_DIR at a shared world-writable path (sessions and OAuth store secrets there).
  • Sessions: same-UID local IPC only; --session-clean-env for untrusted stdio servers.

Agent skill

The agent skill is in skills/skiff/, which npx skills discovers. Install it with:

npx skills add samikh-git/skiff

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 cargo test --test cloudflare_bench -- --ignored --nocapture

Rust vs Python multi-run comparison (dataframe + optional CSV; needs pandas and uvx):

cargo build --release
python3 scripts/bench_vs_python.py --runs 10
# 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 brief may fall through to the full tools cache.
  • Session daemons hold CompactIndex in RAM and answer list_tools_light without shipping inputSchemas. Prefer this for fat servers.
  • Full tools.json remains ~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 streamable and Authorization: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 --toon are 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.