Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,95 @@ jobs:

- name: Build
run: bun build --compile src/cli/index.ts --outfile dist/mcp2cli

- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: mcp2cli-binary
path: dist/mcp2cli
retention-days: 7

deploy:
needs: check
runs-on: self-hosted
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
DEPLOY_HOST: 10.71.20.63
DEPLOY_USER: root
SERVICE_NAME: mcp2cli
BINARY_PATH: /usr/local/bin/mcp2cli
HEALTH_URL: http://10.71.20.63:9500/health

steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Install and build for Linux x64
run: |
bun install --frozen-lockfile
bun build --compile --target=bun-linux-x64 src/cli/index.ts --outfile dist/mcp2cli

- name: Backup current binary
run: |
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"cp ${{ env.BINARY_PATH }} ${{ env.BINARY_PATH }}.bak 2>/dev/null || true"

- name: Deploy new binary
run: |
scp dist/mcp2cli ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/mcp2cli-new
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"mv /tmp/mcp2cli-new ${{ env.BINARY_PATH }} && \
chmod +x ${{ env.BINARY_PATH }} && \
chown mcp2cli:mcp2cli ${{ env.BINARY_PATH }}"

- name: Restart service
run: |
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"systemctl restart ${{ env.SERVICE_NAME }}"

- name: Health check (with retry)
run: |
for i in 1 2 3 4 5; do
sleep 2
if curl -sf --max-time 5 ${{ env.HEALTH_URL }} > /dev/null 2>&1; then
echo "Health check passed (attempt $i)"
exit 0
fi
echo "Health check attempt $i failed, retrying..."
done
echo "Health check failed after 5 attempts"
exit 1

- name: Rollback on failure
if: failure()
run: |
echo "Deployment failed -- rolling back to previous binary"
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"if [ -f ${{ env.BINARY_PATH }}.bak ]; then \
mv ${{ env.BINARY_PATH }}.bak ${{ env.BINARY_PATH }} && \
systemctl restart ${{ env.SERVICE_NAME }} && \
echo 'Rollback complete'; \
else \
echo 'No backup found -- manual intervention required'; \
exit 1; \
fi"

- name: Verify rollback health
if: failure()
run: |
sleep 3
if curl -sf --max-time 5 ${{ env.HEALTH_URL }} > /dev/null 2>&1; then
echo "Rollback health check passed -- service restored"
else
echo "WARNING: Rollback health check failed -- service may be down"
exit 1
fi

- name: Clean up backup
if: success()
run: |
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"rm -f ${{ env.BINARY_PATH }}.bak"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.production.local
.env.local

# auth tokens (secrets)
tokens.json

# caches
.eslintcache
.cache
Expand Down
148 changes: 148 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,154 @@ In addition to the variables listed above, v1.3 adds:
|----------|---------|-------------|
| `MCP2CLI_CACHE_DIR` | `~/.cache/mcp2cli` | Base directory for schema cache and circuit breaker state |

## Network Deployment

mcp2cli can run as a centralized TCP daemon, allowing multiple machines to share a single set of MCP server connections. Install and configure MCP backends once on a server, then connect from any machine using the CLI client or the bash wrapper (curl + jq only -- no Bun required).

### Quick Start (TCP Mode)

**Server** -- start the daemon with TCP binding:

```bash
export MCP2CLI_LISTEN_HOST=0.0.0.0
export MCP2CLI_LISTEN_PORT=9500
export MCP2CLI_AUTH_TOKEN=$(openssl rand -hex 32)
MCP2CLI_DAEMON=1 mcp2cli
```

**Client** -- point any machine at the remote daemon:

```bash
export MCP2CLI_REMOTE_URL=http://mcp-server.local:9500
export MCP2CLI_AUTH_TOKEN=<same-token-as-server>
mcp2cli n8n n8n_list_workflows --params '{}'
```

When `MCP2CLI_REMOTE_URL` is set, the CLI skips local daemon startup entirely and sends requests directly over HTTP.

### Network Environment Variables

In addition to the [base environment variables](#environment-variables), network mode adds:

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP2CLI_LISTEN_HOST` | (unset) | Bind address for TCP mode. Setting this enables TCP instead of Unix socket. Use `0.0.0.0` to listen on all interfaces |
| `MCP2CLI_LISTEN_PORT` | `9500` | TCP port when `MCP2CLI_LISTEN_HOST` is set |
| `MCP2CLI_AUTH_TOKEN` | (unset) | Bearer token for TCP authentication. Required for production deployments |
| `MCP2CLI_REMOTE_URL` | (unset) | URL of remote mcp2cli daemon (e.g. `http://mcp-server:9500`). Enables remote client mode |
| `MCP2CLI_CONFIG` | `~/.config/mcp2cli/services.json` | Path to service definitions (useful for server-side config in `/etc/mcp2cli/`) |

### Authentication

When `MCP2CLI_AUTH_TOKEN` is set on the server, all requests must include a `Bearer` token in the `Authorization` header. The token comparison uses timing-safe equality to prevent timing attacks.

**Auth-exempt paths** -- these skip authentication so load balancers and monitoring can probe without credentials:
- `GET /health` -- health check with uptime, memory, and pool status
- `GET /metrics` -- Prometheus metrics endpoint

### Prometheus Metrics

The daemon exposes metrics at `GET /metrics` in Prometheus text exposition format. Key metrics:

| Metric | Type | Description |
|--------|------|-------------|
| `mcp2cli_requests_total` | counter | Total requests by `{service, tool}` |
| `mcp2cli_requests_errors_total` | counter | Failed requests by `{service, tool}` |
| `mcp2cli_request_duration_ms` | histogram | Request latency with buckets (10ms - 30s) |
| `mcp2cli_requests_active` | gauge | Currently in-flight requests |
| `mcp2cli_pool_connections_active` | gauge | Current connection pool size |
| `mcp2cli_pool_services` | gauge | Connected services (`{service}` label) |
| `mcp2cli_connection_events_total` | counter | Connect/disconnect/health-check-failure by `{service}` |
| `mcp2cli_auth_failures_total` | counter | Total authentication failures |
| `mcp2cli_process_uptime_seconds` | gauge | Daemon uptime |
| `mcp2cli_process_memory_rss_bytes` | gauge | Resident set size |

Add to your Prometheus config:

```yaml
scrape_configs:
- job_name: mcp2cli
static_configs:
- targets: ['mcp-server.local:9500']
```

### Bash Wrapper (curl-only clients)

For machines that only have `curl` and `jq` (no Bun runtime), use the bash wrapper:

```bash
# Install the wrapper
cp scripts/mcp2cli-remote /usr/local/bin/
chmod +x /usr/local/bin/mcp2cli-remote

# Configure
export MCP2CLI_REMOTE_URL=http://mcp-server.local:9500
export MCP2CLI_AUTH_TOKEN=<token>

# Use it like the full CLI
mcp2cli-remote n8n n8n_list_workflows '{}'
```

### LXC Deployment

The `deploy/` directory contains everything needed to run mcp2cli as a systemd service in an LXC container (or any Linux host):

| File | Purpose |
|------|---------|
| `deploy/mcp2cli.service` | systemd unit file (hardened with `NoNewPrivileges`, `ProtectSystem=strict`) |
| `deploy/env.example` | Environment file template -- copy to `/etc/mcp2cli/env` |
| `deploy/services-server.json` | Example server-side service config |

Setup:

```bash
# Copy files into place
cp deploy/mcp2cli.service /etc/systemd/system/
mkdir -p /etc/mcp2cli
cp deploy/env.example /etc/mcp2cli/env
cp deploy/services-server.json /etc/mcp2cli/services.json

# Edit config
vim /etc/mcp2cli/env # set MCP2CLI_AUTH_TOKEN
vim /etc/mcp2cli/services.json # configure your MCP backends

# Enable and start
useradd --system --no-create-home mcp2cli
systemctl daemon-reload
systemctl enable --now mcp2cli
```

### curl Examples

```bash
SERVER=http://mcp-server.local:9500
TOKEN=your-token-here

# Health check (no auth required)
curl -s $SERVER/health | jq .

# Prometheus metrics (no auth required)
curl -s $SERVER/metrics

# List tools for a service
curl -s -X POST $SERVER/list-tools \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"service": "n8n"}' | jq .

# Invoke a tool
curl -s -X POST $SERVER/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"service": "n8n", "tool": "n8n_list_workflows", "params": {}}' | jq .

# Get a tool schema
curl -s -X POST $SERVER/schema \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"service": "n8n", "tool": "n8n_list_workflows"}' | jq .
```

## Development

```bash
Expand Down
33 changes: 33 additions & 0 deletions deploy/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# mcp2cli Daemon Configuration
# Copy to /etc/mcp2cli/env and edit values

# Enable daemon mode (required)
MCP2CLI_DAEMON=1

# Network binding -- listen on all interfaces
MCP2CLI_LISTEN_HOST=0.0.0.0
MCP2CLI_LISTEN_PORT=9500

# Authentication token for TCP connections
# Generate a strong token and store in vaultwarden
# Example: openssl rand -hex 32
MCP2CLI_AUTH_TOKEN=CHANGE_ME_USE_VAULTWARDEN

# Path to service definitions
MCP2CLI_CONFIG=/etc/mcp2cli/services.json

# Logging level: debug, info, warn, error
MCP2CLI_LOG_LEVEL=info

# Idle timeout in seconds before shutting down inactive backends
# Defaults to 0 (disabled) in TCP mode -- backends stay alive
# MCP2CLI_IDLE_TIMEOUT=0

# Maximum concurrent backend processes in the pool
# MCP2CLI_POOL_MAX=10

# Per-tool invocation timeout in seconds
# MCP2CLI_TOOL_TIMEOUT=30

# Directory for schema and response caching
# MCP2CLI_CACHE_DIR=/var/lib/mcp2cli/cache
21 changes: 21 additions & 0 deletions deploy/mcp2cli.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[Unit]
Description=mcp2cli MCP Bridge Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=mcp2cli
Group=mcp2cli
EnvironmentFile=/etc/mcp2cli/env
ExecStart=/usr/local/bin/mcp2cli
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mcp2cli /var/log/mcp2cli
PrivateTmp=true

[Install]
WantedBy=multi-user.target
31 changes: 31 additions & 0 deletions deploy/services-server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"services": {
"n8n": {
"backend": "stdio",
"command": "npx",
"args": ["-y", "@n8n/n8n-mcp"],
"env": {
"N8N_HOST": "http://10.71.20.30:5678",
"N8N_API_KEY": "${N8N_API_KEY}"
}
},
"vaultwarden-secrets": {
"backend": "http",
"url": "http://10.71.20.14:3001/mcp"
},
"notebooklm-mcp": {
"backend": "stdio",
"command": "uvx",
"args": ["notebooklm-mcp"],
"env": {}
},
"homekit": {
"backend": "http",
"url": "http://10.71.1.69:9234/mcp",
"fallback": {
"command": "echo",
"args": ["homekit service not reachable"]
}
}
}
}
Loading