mcp2cli
About
CLI bridge that wraps MCP servers as bash-invokable commands, recovering ~11K tokens of context window per session https://github.com/rodaddy/mcp2cli
Details
- Author
- rodaddy
- Categories
- Developer Tools, Automation, Productivity
Jump to
Setup
Install mcp2cli in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/rodaddy/mcp2cli
Follow the installation instructions in the repository README, then restart your MCP client.
CLI bridge that wraps MCP (Model Context Protocol) servers as bash-invokable commands. Instead of loading all MCP tool definitions into an LLM's system prompt (~13K+ tokens permanently), agents invoke tools via bash at zero context cost.
# Install git clone <repo-url> cd mcp2cli bun install bun run build # produces dist/mcp2cli # Bootstrap from existing Claude config mcp2cli bootstrap # reads ~/.claude.json mcpServers -> ~/.config/mcp2cli/services.json # Use it mcp2cli services # list available services mcp2cli n8n --help # list tools for a service mcp2cli n8n n8n_list_workflows --params '{}' # invoke a tool mcp2cli schema n8n.n8n_list_workflows # inspect tool schema
bun run dev -- services bun run dev -- n8n n8n_list_workflows --params '{}'
git clone <repo-url> cd mcp2cli bun install bun run build
The compiled binary lands atdist/mcp2cli. Add it to your PATH or symlink it.
On macOS, do not overwrite an existing compiled binary in place withcp new dist/mcp2cli. Replacing the contents of the same inode can invalidate the ad-hoc code signature and cause the next exec to be killed withSIGKILL/ exit code 137.
rm dist/mcp2cli cp /path/to/new/mcp2cli dist/mcp2cli
If the local UI daemon is managed by launchd, restart it after replacing the binary:
launchctl kickstart -k gui/501/com.mcp2cli.local-ui
The already-running daemon keeps the old inode open until restart, so this is safe to do while the daemon is live.
mcp2cli discovers MCP servers from~/.config/mcp2cli/services.json:
{ "services": { "n8n": { "description": "n8n workflow automation", "backend": "stdio", "command": "npx", "args": ["-y", "@anthropic-ai/n8n-mcp"], "env": { "N8N_BASE_URL": "https://n8n.example.com", "N8N_API_KEY": "your-api-key" } }, "homekit": { "description": "HomeKit smart home control", "backend": "stdio", "command": "node", "args": ["/path/to/homekit-mcp/dist/index.js"], "env": {} } } }
Each service entry mirrors the Claude DesktopmcpServersformat -- samecommand,args, andenvfields.
If you already have MCP servers configured in~/.claude.json:
This reads yourmcpServersentries and generatesservices.jsonautomatically.
mcp2cli <service> <tool> --params '<json>'
The--paramsvalue must be valid JSON matching the tool's input schema.
mcp2cli schema <service>.<tool>
Returns the JSON Schema for the tool's input parameters -- useful for discovering required fields.
mcp2cli <service> <tool> --params '{"query": "test"}' --dry-run
Validates input and shows what would be sent without executing the tool call.
mcp2cli <service> <tool> --params '{}' --fields "id,name,status"
Extracts only the specified fields from the response -- reduces output noise for scripting.
Generates PAI skill files from MCP tool schemas, making tools discoverable by AI agents.
mcp2cli daemon status # check if daemon is running, connection pool stats mcp2cli daemon stop # graceful shutdown
All responses are structured JSON on stdout. Logs go to stderr.
// Success { "success": true, "result": { "workflows": [...] } } // Error { "error": true, "code": "TOOL_ERROR", "message": "Workflow not found", "reason": "..." }
This makes mcp2cli composable withjq, pipes, and scripting:
# Get workflow names mcp2cli n8n n8n_list_workflows --params '{}' | jq '.result.workflows[].name' # Check for errors mcp2cli n8n n8n_get_workflow --params '{"id": "123"}' | jq 'if .error then .message else .result end'
mcp2cli n8n n8n_get_workflow --params '{"id": "123"}' 2>/dev/null if [ $? -eq 4 ]; then echo "Connection failed -- is the MCP server configured?" fi
MCP2CLI_LOG_LEVEL=debug mcp2cli n8n n8n_list_workflows --params '{}' MCP2CLI_NO_DAEMON=1 mcp2cli n8n n8n_list_workflows --params '{}'
A slow verb passes throughtwo independent deadlines, and raising only one changes nothing — the other still fires first:
- Daemon → MCP server.The per-servicetimeoutinservices.json(falling back toMCP2CLI_TOOL_TIMEOUT, default 60s) is handed to the MCP SDK on every tool call. Without it the SDK applies its ownDEFAULT_REQUEST_TIMEOUT_MSECof 60s and fails withMCP error -32001: Request timed out.
- CLI → daemon.MCP2CLI_REQUEST_TIMEOUT_MS(default 60s) bounds the local Unix-socket request. When it fires the CLI reportsCONNECTION_ERROR: The operation timed out.even though the daemon and the MCP server are still working.
So a verb that can run for 20 minutes needs both:
// services.json { "services": { "runner-boxes": { "timeout": 1200000 / .../ } } }
MCP2CLI_REQUEST_TIMEOUT_MS=1200000 mcp2cli runner-boxes runner_done --params '{}'
A timed-out call isnota failed verb: the server-side work may well have completed after the client stopped waiting. Do not blind-retry a non-idempotent verb on a timeout.
CLI Entry (src/cli/index.ts) |-- Command Dispatch (services, schema, bootstrap, generate-skills, daemon) |-- Tool Call Handler -> Daemon Client (Unix socket) | \-- Daemon Server (src/daemon/server.ts) | |-- Connection Pool (src/daemon/pool.ts) | | \-- MCP Transport (src/connection/transport.ts) | |-- Idle Timer (src/daemon/idle.ts) | \-- Health Endpoint (/health with memory stats) |-- Input Validation (src/validation/) -- 48 adversarial patterns |-- Schema Introspection (src/schema/) |-- Skill Generation (src/generation/) \-- Structured Logger (src/logger/) -- JSON on stderr
Persistent daemon.MCP servers have a 2-5 second startup cost per connection. The daemon keeps connections alive in a pool, so subsequent calls return in milliseconds instead of seconds. The daemon auto-exits after the idle timeout (default 60s).
Connection pool with health checks.Connections are validated before use and recycled on failure. The pool enforces a max size to prevent resource exhaustion.
Structured JSON everywhere.stdout is always parseable JSON -- no mixed text output. Logs (when enabled) go to stderr as structured JSON lines. This makes mcp2cli reliable for scripting and piping.
Semantic exit codes.Different failure modes get different exit codes so callers can branch on the type of error without parsing output.
Input validation.All tool parameters are validated against the MCP schema before the call is dispatched. The validation layer handles 48 adversarial patterns (injection attempts, type coercion, overflow) to fail fast with clear errors.
mcp2cli is designed to be called from AI agents via bash tool use. A typical agent workflow:
# Agent discovers available tools mcp2cli n8n --help # Agent reads the schema to understand parameters mcp2cli schema n8n.n8n_get_workflow # Agent invokes the tool mcp2cli n8n n8n_get_workflow --params '{"id": "abc123"}'
This pattern keeps MCP tool definitions out of the agent's system prompt entirely. The agent only pays context cost when it actually needs to call a tool, and even then only for the specific tool's schema -- not all tools from all servers.
mcp2cli supports multi-user RBAC via~/.config/mcp2cli/tokens.json. Each user or agent gets a bearer token with a role.
{ "tokens": [ { "id": "rico", "token": "your-admin-token-here", "role": "admin", "description": "Full admin access", "username": "rico", "password": "your-web-ui-password", "expiresAt": "2026-07-01T00:00:00.000Z" }, { "id": "skippy", "token": "your-agent-token-here", "role": "agent", "description": "AI agent - tools + read, no config mutations", "expiresAt": "2026-07-01T00:00:00.000Z" }, { "id": "viewer01", "token": "your-viewer-token-here", "role": "viewer", "description": "Read-only access" } ] }
Generate secure tokens:openssl rand -base64 32
Theusername/passwordfields enable web UI login at the daemon's root URL. Token-based auth (Bearer header) works for all API and CLI access.
The optionalexpiresAtfield enables token expiry and refresh. Expired tokens are rejected. Near-expiry tokens fromtokens.jsoncan be rotated throughPOST /api/auth/refresh; the daemon writes the new token back totokens.jsonand hot-reloads token file edits. Local CLI clients proactively refresh near-expiry admin tokens before daemon API calls.
- No tokens.json, no env token:Auth disabled, all requests treated as admin (backward compatible)
- MCP2CLI_AUTH_TOKENenv var only:Legacy single-token mode, treated as admin
- tokens.json exists:Full multi-user RBAC
Different users and agents can have their own API keys for backend services. When rico calls open-brain, he uses his key. When skippy calls it, the agents' shared key is used.
Create~/.config/mcp2cli/credentials.json:
{ "groups": { "ai_agents": ["skippy", "bilby", "nagatha", "claude"] }, "credentials": { "rico": { "open-brain": { "headers": { "Authorization": "Bearer ricos-ob-key" } } }, "ai_agents": { "open-brain": { "headers": { "Authorization": "Bearer agents-shared-ob-key" } }, "n8n": { "env": { "N8N_API_KEY": "agents-n8n-key" } } } }, "defaults": { "proxmox": { "headers": { "Authorization": "PVEAPIToken=shared-token" } } } }
When a tool call comes in, credentials are resolved in priority order:
- User-specific--credentials[userId][service]
- Group-- first matching group the user belongs to
- Defaults--defaults[service]
- services.json-- whatever's baked into the service config (backward compatible)
For http/websocket services, credential headers are merged into the connection. For stdio services, credential env vars are merged into the process environment.
For identity-sensitive services, setrequiresCredentials: trueinservices.json. If no user, group, or explicit default credential exists, the daemon rejects the call instead of using base service headers.
# Set credentials for an identity on a service mcp2cli credentials set rico open-brain --header "Authorization: Bearer my-key" # Set env-based credentials (for stdio services) mcp2cli credentials set rico n8n --env "N8N_API_KEY=my-n8n-key" # Set a default credential (used when no user/group match) mcp2cli credentials set-default proxmox --header "Authorization: PVEAPIToken=shared" # List all credentials (values are redacted) mcp2cli credentials list # Show effective credential source for a user mcp2cli credentials resolve skippy open-brain # → {"exists": true, "source": "group"} # Group management mcp2cli credentials group add ai_agents skippy bilby nagatha mcp2cli credentials group add-members ai_agents claude mcp2cli credentials group remove-members ai_agents bilby mcp2cli credentials group list # Remove credentials mcp2cli credentials remove rico open-brain mcp2cli credentials remove-default proxmox mcp2cli credentials group remove ai_agents # Reload from disk after manual edits mcp2cli credentials reload # Populate Open Brain credentials from a Vaultwarden item mcp2cli credentials bootstrap-open-brain --item "Open Brain - Per-User Tokens"
Open Brain (OBv2) is an HTTP MCP service where the bearer token controls namespace identity. Do not put an Open BrainAuthorizationheader inservices.json; store it only as a per-identity credential.
services.json-- base config for a hosted daemon (no credentials, endpoint only):
{ "services": { "open-brain": { "backend": "http", "url": "http://open-brain.example.internal:3100/mcp", "source": "remote", "requiresCredentials": true, "preconnect": false } } }
Usesource: "remote"when the CLI is routing through a hosted mcp2cli daemon.requiresCredentials: truemakes missing per-identity credentials fail closed, andpreconnect: falseprevents daemon startup from opening an unauthenticated base Open Brain connection.
{ "groups": { "ai_agents": ["skippy", "bilby", "claude"] }, "credentials": { "rico": { "open-brain": { "headers": { "Authorization": "Bearer ricos-ob-api-key" } } }, "ai_agents": { "open-brain": { "headers": { "Authorization": "Bearer agents-shared-ob-key" } } } } }
Now when rico callsmcp2cli open-brain search_all --params '{"query": "kubernetes"}', his personal key is injected. When skippy calls the same tool, the agents' shared key is used. Each gets their own connection in the pool.
Vaultwarden bootstrap-- if the itemOpen Brain - Per-User Tokenshas custom fields such asAUTH_TOKEN_USER_RICO,AUTH_TOKEN_USER_SKIPPY, andAUTH_TOKEN_USER_BILBY, run:
mcp2cli credentials bootstrap-open-brain
The field suffix is lowercased and used as the identity (AUTH_TOKEN_USER_RICO->rico). Existing credentials are skipped unless--forceis passed. The command prints counts and identity names only; it does not print bearer tokens.
Header and env values support${caller.id}and${caller.role}template variables. These are replaced with the authenticated caller's identity at call time.
In services.json-- inject identity headers for services whose backend trusts caller metadata instead of per-user bearer tokens:
{ "services": { "example-service": { "backend": "http", "url": "https://example.internal/mcp", "headers": { "X-Agent-Id": "${caller.id}", "X-Role": "${caller.role}" } } } }
When bilby calls the service, the request headers becomeX-Agent-Id: bilbyandX-Role: agent.
In credentials.json-- combine per-identity keys with identity headers:
{ "credentials": { "rico": { "open-brain": { "headers": { "Authorization": "Bearer ricos-ob-key", "X-Namespace": "${caller.id}" } } } } }
Templates work in both headers and env values. Unknown variables (e.g.,${caller.email}) are left unexpanded.
- Redacted list output--GET /api/credentialsreturnsBearnot full values
- IDOR protection-- agents can only resolve their own credentials, admin required for others
- File permissions--credentials.jsonis written with0600(owner read/write only)
- Input validation-- header values reject CRLF injection, dangerous headers (Host, Transfer-Encoding) and env vars (PATH, LD_PRELOAD, NODE_OPTIONS) are blocked
- Atomic writes-- temp file + rename prevents partial writes on crash
- Pool invalidation*-- changing credentials evicts stale connections automatically
Schemas are cached locally to avoid re-fetching on every invocation. Cached schemas live at~/.cache/mcp2cli/schemas/with a 24-hour TTL. Cache drift is detected via SHA-256 hashing -- if the upstream schema changes, the cache is automatically invalidated.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.


