npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cli2mcp-gateway

v1.0.1

Published

Wrap any CLI as a Streamable HTTP MCP server with bearer / OAuth 2.1 / Auth0 auth, CORS, and rate limiting.

Downloads

356

Readme

cli2mcp-gateway

Wrap any CLI as a Streamable HTTP MCP server with bearer / OAuth 2.1 / Auth0 auth, CORS, and rate limiting. Dual transport: same CLI, exposed as either stdio MCP (for local agents) or HTTP MCP (for remote / team use).


What it does

cli2mcp-gateway wraps any CLI as an MCP server. On startup it runs <cli> --help and then walks nested command trees with <cli> <path...> --help until it reaches a leaf command, infers a JSON Schema, and exposes one MCP tool per leaf command.

| Transport | Use case | Entry point | |---|---|---| | stdio | Local agents (Claude Code, Codex, Cursor) spawning one process per session | cli2mcp-gateway serve | | HTTP | Remote agents hitting a shared bearer / OAuth / Auth0 endpoint | cli2mcp-gateway serve --http |

The business layer (createServer) is shared between transports. Only the transport plumbing differs.

The CLI is spawned directly via execa() — no nested MCP client, no extra process hop. Schema inference lives in src/help-parser.js.

Windows note: the published command uses a small Node launcher so cli2mcp-gateway runs as a program instead of opening src/server.mjs in an editor.

Example: parsehub becomes 4 MCP tools

parsehub_parse      ← matches `parsehub parse <url>`
parsehub_download   ← matches `parsehub download <url> -o <path>`
parsehub_platforms  ← matches `parsehub platforms`
parsehub_set        ← matches `parsehub set <sub-subcommand> ...`

Each tool's inputSchema is auto-derived from the corresponding --help output: positional arguments become a positional string array; flags with arg hints (e.g. -o PATH) become typed strings; boolean flags stay boolean.


Install

git clone https://github.com/drtx32/cli2mcp-gateway.git
cd cli2mcp-gateway
npm install
cp .env.example .env   # then edit .env

Requires Node.js ≥ 20.


Quick start — HTTP mode

The default. Lets any MCP-aware HTTP client reach your CLI remotely.

Option A: local install + .env file

git clone https://github.com/drtx32/cli2mcp-gateway.git
cd cli2mcp-gateway
npm install
cp .env.example .env   # then edit .env
npm start

Option B: global install + env vars on the command line (no .env file)

After the first npm release:

npm install -g cli2mcp-gateway

Before the npm package is bootstrapped, install directly from GitHub:

npm install -g drtx32/cli2mcp-gateway

Then start it with env vars inline:

# one-shot start with env inline:
MCP_TOKEN=ph_token_12345678 \
CLI_COMMAND=parsehub \
CLI_CWD=/tmp/parsehub-downloads \
HOST=127.0.0.1 \
PORT=3101 \
PUBLIC_ENDPOINT=http://127.0.0.1:3101 \
cli2mcp-gateway serve --http

Or export once into your shell profile (~/.zshrc / ~/.bashrc):

export MCP_TOKEN=ph_token_12345678
export CLI_COMMAND=parsehub
export CLI_CWD=/tmp/parsehub-downloads
export HOST=127.0.0.1
export PORT=3101
export PUBLIC_ENDPOINT=http://127.0.0.1:3101

cli2mcp-gateway serve --http

serve flags

serve accepts only these mode switches:

  • --http to use HTTP transport
  • --stdio to force stdio transport

If you pass neither flag, serve defaults to stdio. winapp is not a supported subcommand.

The .env file is purely a convenience — every variable it can hold can be passed via the shell instead.

Sanity check:

curl http://127.0.0.1:3101/health
# → {"ok":true,"authMode":"bearer","toolNames":["parsehub_parse","parsehub_download","parsehub_platforms","parsehub_set"],"cliResolved":true}

Full MCP handshake from any HTTP client (curl example):

# 1) initialize → get session id
SID=$(curl -s -i -X POST http://127.0.0.1:3101/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"x","version":"0"}},"id":1}' \
  | grep -i "^mcp-session-id:" | awk '{print $2}' | tr -d '\r')

# 2) ack
curl -X POST http://127.0.0.1:3101/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $MCP_TOKEN" -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3) call tools
curl -X POST http://127.0.0.1:3101/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $MCP_TOKEN" -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'

Releases

CI runs the test suite on Node.js 22 and 24 for pull requests and pushes to main. Pushing a v* tag runs the release checks and publishes the matching package version to npm through GitHub Actions trusted publishing (OIDC). The initial npm publication and trusted-publisher setup are documented in docs/releasing.md. The npm publish workflow does not require a GitHub Deployment or a long-lived npm token.


Quick start — stdio mode

For local agents that spawn one MCP process per session. No HTTP, no token — the parent agent is trusted.

npm run stdio

Or via Claude Code / Codex MCP config:

{
  "mcpServers": {
    "my-cli": {
      "command": "node",
      "args": ["/path/to/cli2mcp-gateway/bin/cli2mcp-gateway.cjs", "serve"]
    }
  }
}

Configuration (env vars)

All config flows through environment variables. See .env.example for the full list. Key ones:

| Var | Default | Notes | |---|---|---| | HOST | 127.0.0.1 | 0.0.0.0 to expose on LAN | | PORT | 3100 | HTTP listen port | | AUTH_MODE | bearer | bearer | oauth | both | | MCP_TOKEN | (random per boot) | Static bearer token. Set in production. | | CLI_COMMAND | rg | Downstream CLI binary on $PATH. Must support --help. | | CLI_CWD | process cwd | Working directory passed to the CLI | | CLI_SUBCOMMANDS | (auto) | Comma-separated list. Empty = auto-discover from <cli> --help. | | CLI_TIMEOUT_MS | 60000 | Per-call CLI timeout (execa). | | PUBLIC_ENDPOINT | http://$HOST:$PORT | Required https://... when OAuth enabled | | OAUTH_USERNAME / OAUTH_PASSWORD | admin / change-me | Replace before any non-local use | | AUTH0_ISSUER / AUTH0_AUDIENCE | (empty) | Enables Auth0 JWT verification | | ALLOWED_HOSTS | (empty) | Host header allowlist (anti-DNS-rebinding) | | MCP_ALLOWED_IPS | (empty) | Source-IP allowlist. Comma-separated. Accepts bare IP (1.2.3.4), IP:port (1.2.3.4:3101), CIDR (10.0.0.0/8), and IPv6. Empty = allow all. | | CORS_ORIGINS | (empty) | Comma-separated allowlist; empty = browser clients blocked | | RATE_LIMIT_RPS / RATE_LIMIT_BURST | 20 / 40 | Per-IP token bucket | | STDIO_BOOT_TIMEOUT_SEC | 0 | 0 = wait forever for <cli> --help (recommended) | | STDIO_IDLE_TIMEOUT_SEC | 0 | 0 = no idle exit (stdio only) |

Auth modes

  • bearer — single static MCP_TOKEN. Easiest for personal/team use.
  • oauth — full OAuth 2.1 with PKCE, dynamic client registration, refresh tokens. Use this for ChatGPT / Claude Desktop / browser clients.
  • both — accept either. Handy during migration.
  • Auth0 — add AUTH0_ISSUER + AUTH0_AUDIENCE to verify Auth0 JWTs alongside the others.

OAuth is required to be served over HTTPS in non-localhost deployments; the gateway refuses to start otherwise.

Multi-service box.yaml mode

For multiple upstreams, use a box.yaml file. The gateway supports these inbound adapters:

  • cli — a non-MCP CLI. The gateway can expose help + run (tool_mode: dual) or help + recursively discovered leaf commands (the default).
  • mcp-stdio — an MCP server launched as a stdio child process. For a CLI whose MCP entry point is a subcommand, put that subcommand in args, for example args: [mcp].
  • mcp-http — an upstream Streamable HTTP MCP server. Use headers for bearer tokens or other gateway-to-gateway headers; auth.token can generate an Authorization: Bearer ... header.
  • auto — probe the command as MCP stdio first (including a conventional mcp subcommand), then fall back to the help-driven cli adapter.

Tool exposure mode (tool_mode)

Each cli / auto / mcp-stdio / mcp-http service can declare how the gateway exposes it. Pick one:

| tool_mode | Tools exposed | workspace.path | |--------------|----------------------------------------------------------------------------------------------|------------------| | normal | All recursively-discovered leaf tools + synthetic help. Optionally adds <cli>_workspace if workspace.path is set. | optional | | dual | Only <cli>_help and <cli>_run. workspace.path is ignored even if configured. | ignored | | triple | Exactly <cli>_help, <cli>_run, and <cli>_workspace. workspace.path is required — boot fails with a clear error otherwise. | required |

Legacy dual_tool_mode: true still works and is equivalent to tool_mode: dual. When both flags are present, the explicit tool_mode wins.

The names in the table are per-service tool names. With the default multi-service aggregation, the public name also includes the service name and __ separator; the example below shows the resulting names.

Managed workspace

When workspace.path is configured and tool_mode is not dual, the gateway exposes a synthetic <cli>_workspace tool that gives the agent read-only access to a single canonical absolute directory. Subcommands:

  • <cli>_workspace(subcommand="dir") — returns the canonical absolute path so the agent can pass it directly to downstream CLI flags like --output, -o, --save.
  • <cli>_workspace(subcommand="list", path?, recursive?) — bounded recursive listing. max_list_entries caps the result.
  • <cli>_workspace(subcommand="stat", path) — metadata for one entry.
  • <cli>_workspace(subcommand="read", path) — returns the file as a typed MCP content block: image content for PNG / JPEG / GIF / WebP, text content for UTF-8 / JSON / markdown / etc., and a structured error for anything binary or oversized.

The workspace is strictly read-only: no write / delete / move / copy / execute support, and the gateway never fetches URLs into the workspace. .., absolute paths outside the configured root, symlinks that resolve outside the root, and hidden files (leading .) are all rejected with a clear actionable error before any read. follow_symlinks defaults to false; when set to true, every descent still verifies containment. max_read_bytes enforces a hard cap on file reads — the gateway never silently truncates file content.

Relative paths in workspace.path resolve against the directory holding box.yaml, not against process cwd. The directory is auto-created at boot if missing.

Example:

name: my-box
services:
  local_cli:
    adapter: cli
    command: my-cli
    tool_mode: triple
    workspace:
      path: ./workspace-local
      max_read_bytes: 16777216
      max_list_entries: 200
      follow_symlinks: false
      allow_hidden_files: false
  pure_mcp:
    adapter: mcp-stdio
    command: npx
    args: [-y, my-mcp-server]
  remote_mcp:
    adapter: mcp-http
    url: https://mcp.example.com/mcp
    auth:
      type: bearer
      token: ${UPSTREAM_MCP_TOKEN}
transport:
  type: http
  host: 127.0.0.1
  port: 3100

In triple mode the agent sees three tools: local_cli__my-cli_help, local_cli__my-cli_run, and local_cli__my-cli_workspace with the example above and default naming. The agent is expected to call local_cli__my-cli_workspace(subcommand="dir") to discover the canonical absolute workspace root, and then pass it to the downstream CLI's output / save flags. The downstream CLI does not need to read any CLI2MCP_* environment variable.

Local runtime management

serve remains the foreground entrypoint. For Windows-friendly background management, register HTTP boxes with the local runtime daemon:

cli2mcp-gateway create --name my-box --config .\box.yaml --start
cli2mcp-gateway list
cli2mcp-gateway get <id>
cli2mcp-gateway logs <id> --tail 100
cli2mcp-gateway restart <id>
cli2mcp-gateway stop <id>
cli2mcp-gateway start <id>
cli2mcp-gateway update <id> --config .\box-v2.yaml
cli2mcp-gateway rename <id> my-box-v2
cli2mcp-gateway delete <id>

The daemon and managed gateway processes are detached from the terminal and use a per-user state directory (%LOCALAPPDATA%\cli2mcp-gateway on Windows, or $XDG_STATE_HOME/cli2mcp-gateway on other platforms). get includes the registered instance metadata and the current YAML text. Runtime management is intended for http/sse boxes; use serve directly for stdio mode.


How a downstream tool call flows

HTTP client                          gateway                       downstream CLI
  │                                    │                                  │
  │  POST /mcp  (Bearer + JSON-RPC)    │                                  │
  ├───────────────────────────────────▶│                                  │
  │                                    │  execa(cmd, [sub, --flag value]) │
  │                                    ├─────────────────────────────────▶│
  │                                    │                                  │  spawn $CLI_COMMAND
  │                                    │                                  │  cwd = $CLI_CWD
  │                                    │                                  │  args = buildArgv(shape, args)
  │                                    │◀─────────────────────────────────┤
  │                                    │  CLI stdout / stderr             │
  │◀───────────────────────────────────┤                                  │
  │  200 + SSE / JSON                  │                                  │

buildArgv(shape, args) translates the MCP tools/call.arguments object into argv:

  • Each flag defined in the inferred schema becomes --<long> <value> when truthy / non-empty.
  • Positionals come from arguments.positional (a string array) in declared order.

Concrete example

Calling parsehub_download to fetch a Xiaohongshu note:

{
  "name": "parsehub_download",
  "arguments": {
    "positional": ["https://www.xiaohongshu.com/discovery/item/6a5ccd97..."],
    "o": "/tmp/downloads"
  }
}

becomes:

parsehub download https://www.xiaohongshu.com/discovery/item/6a5ccd97... -o /tmp/downloads

The CLI's stdout (file list, JSON, etc.) is returned verbatim as the MCP tool result.


Why a custom gateway?

Wrapping a CLI as an MCP tool has its own set of pain points beyond what the raw CLI provides. This gateway adds:

  • HTTP transport with bearer / OAuth 2.1 / Auth0 (raw CLIs are stdio only)
  • Per-subcommand tool discovery from --help output (no manual schema authoring)
  • Per-IP rate limiting
  • Graceful shutdown with drain timeout
  • stdio lifecycle hardening (parent watchdog, idle timeout, boot timeout)
  • Centralized env-driven config with sensible defaults

Files

cli2mcp-gateway/
├── src/
│   ├── server.mjs          # dual-transport entry + HTTP server
│   ├── box-config.mjs      # YAML box loader + zod schema
│   ├── tool-aggregator.mjs # per-service → flat tool list merger
│   ├── workspace.mjs       # managed single-root read-only workspace runtime
│   ├── help-parser.js      # argparse-style --help → JSON Schema inference
│   ├── service-env.mjs     # per-service env merging (strips task tokens)
│   └── adapters/           # mcp-stdio / mcp-http upstream adapters
├── test/                    # node --test specs
├── test-fixtures/          # stub MCP servers for e2e tests
├── .env.example
├── .gitignore
├── package.json
├── README.md
└── LICENSE

The CLI runs in whatever directory you point it at via CLI_CWD — no sandbox/ is created by default. Create that path yourself if you want a sandboxed workspace. For multi-service box.yaml deployments, configure a workspace.path per service instead — see Managed workspace.


TODO

  • [ ] Normalize generated MCP tool names to identifier-safe names: convert - and other non-identifier characters in CLI command path segments to _ for the MCP-facing tool name, while preserving the original CLI command path for dispatch. Example: ashare jygs industrial-chains → MCP tool ashare_jygs__industrial_chains, with dispatch path ['jygs', 'industrial-chains'].
  • [ ] Add regression tests covering nested commands with hyphens to ensure tool discovery, tools/list, and tools/call remain compatible across MCP clients that map tool names to language-level function identifiers.

License

MIT © drtx32