claude-ide-bridge 2.22.7 → 2.22.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/README.md +172 -0
- package/deploy/bootstrap-new-vps.sh +364 -0
- package/deploy/claude-ide-bridge.service.template +67 -0
- package/deploy/claude-ide-bridge@.service +31 -0
- package/deploy/ecosystem.config.js.example +36 -0
- package/deploy/install-vps-service.sh +240 -0
- package/deploy/nginx-claude-bridge.conf.template +129 -0
- package/package.json +4 -2
- package/scripts/gen-claude-desktop-config.sh +124 -0
package/deploy/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Deploy
|
|
2
|
+
|
|
3
|
+
Production VPS deployment files for claude-ide-bridge.
|
|
4
|
+
|
|
5
|
+
> **Deployment targets:** The **systemd + nginx** path (this directory) is the
|
|
6
|
+
> production-supported deployment for VPS/remote use. A `Dockerfile` and
|
|
7
|
+
> `docker-compose.yml` exist in the repo root as an alternative for
|
|
8
|
+
> containerised environments, but are not actively tested against the systemd
|
|
9
|
+
> config — if you use Docker, treat it as a community-supported path and keep
|
|
10
|
+
> it in sync with any service config changes.
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
| File | Purpose |
|
|
15
|
+
|------|---------|
|
|
16
|
+
| `bootstrap-new-vps.sh` | **Full fresh-server setup** — Node.js, clone, build, user, firewall, systemd, nginx, Certbot |
|
|
17
|
+
| `install-vps-service.sh` | **Idempotent updater** — re-installs service + nginx after `git pull` on an existing server |
|
|
18
|
+
| `nginx-claude-bridge.conf.template` | nginx config reference (domain + port injected by scripts) |
|
|
19
|
+
| `claude-ide-bridge.service.template` | systemd unit reference (paths + user injected by scripts) |
|
|
20
|
+
| `claude-ide-bridge@.service` | Template unit for multi-user demo instances (alternate pattern) |
|
|
21
|
+
| `ecosystem.config.js.example` | PM2 ecosystem config template (alternative to systemd) |
|
|
22
|
+
|
|
23
|
+
## First-time setup (new VPS)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Option A: run remotely on fresh server
|
|
27
|
+
DOMAIN=bridge.example.com bash <(curl -fsSL https://raw.githubusercontent.com/Oolab-labs/claude-ide-bridge/main/deploy/bootstrap-new-vps.sh)
|
|
28
|
+
|
|
29
|
+
# Option B: after cloning the repo
|
|
30
|
+
DOMAIN=bridge.example.com bash deploy/bootstrap-new-vps.sh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The bootstrap script handles everything end-to-end:
|
|
34
|
+
1. Installs Node.js 20, nginx, certbot
|
|
35
|
+
2. Creates a dedicated `claude-bridge` system user (non-root)
|
|
36
|
+
3. Clones the repo to `/opt/claude-ide-bridge`
|
|
37
|
+
4. Runs `npm ci && npm run build`
|
|
38
|
+
5. Generates `.env.vps` with a random auth token
|
|
39
|
+
6. Opens ports 80/443 in ufw
|
|
40
|
+
7. Installs and enables the systemd service
|
|
41
|
+
8. Writes the nginx config with your domain injected
|
|
42
|
+
9. Runs Certbot for HTTPS
|
|
43
|
+
10. Starts the bridge and confirms it's healthy
|
|
44
|
+
|
|
45
|
+
**Required:** `DOMAIN` env var pointing to a subdomain that resolves to your VPS IP.
|
|
46
|
+
|
|
47
|
+
**Optional overrides:**
|
|
48
|
+
|
|
49
|
+
| Variable | Default | Description |
|
|
50
|
+
|----------|---------|-------------|
|
|
51
|
+
| `REPO_URL` | GitHub upstream | Git repo to clone |
|
|
52
|
+
| `INSTALL_DIR` | `/opt/claude-ide-bridge` | Where to install |
|
|
53
|
+
| `SERVICE_USER` | `claude-bridge` | System user to run as |
|
|
54
|
+
| `PORT` | `9000` | Bridge listen port |
|
|
55
|
+
| `BRANCH` | `main` | Git branch |
|
|
56
|
+
| `SKIP_CERTBOT` | `0` | Set to `1` to skip TLS (DNS not ready) |
|
|
57
|
+
|
|
58
|
+
## Updating an existing server
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd /opt/claude-ide-bridge # or wherever INSTALL_DIR is
|
|
62
|
+
git pull
|
|
63
|
+
npm ci
|
|
64
|
+
npm run build
|
|
65
|
+
bash deploy/install-vps-service.sh
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`install-vps-service.sh` re-generates the systemd unit and nginx config from the current state of `.env.vps`, then restarts the service automatically. It auto-detects the domain and service user from the existing installation — no config needed.
|
|
69
|
+
|
|
70
|
+
## Day-to-day management
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# View live logs
|
|
74
|
+
journalctl -u claude-ide-bridge -f
|
|
75
|
+
|
|
76
|
+
# Check status
|
|
77
|
+
systemctl status claude-ide-bridge
|
|
78
|
+
|
|
79
|
+
# Restart after code change
|
|
80
|
+
npm run build && systemctl restart claude-ide-bridge
|
|
81
|
+
|
|
82
|
+
# Stop (won't restart until manually started)
|
|
83
|
+
systemctl stop claude-ide-bridge
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Using PM2 instead of systemd
|
|
87
|
+
|
|
88
|
+
PM2 is a simpler alternative when you're running as root, already have PM2 installed, or prefer not to configure systemd manually.
|
|
89
|
+
|
|
90
|
+
> **Important:** The bridge picks a **random port** by default. The port in your PM2 start command **must match** the port in your nginx `proxy_pass` directive, or every restart will cause a 502 Bad Gateway.
|
|
91
|
+
|
|
92
|
+
### Quick start with PM2
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Replace 4748 and YOUR_TOKEN_HERE with your nginx proxy_pass port and auth token
|
|
96
|
+
pm2 delete claude-bridge 2>/dev/null || true
|
|
97
|
+
pm2 start /root/claude-ide-bridge/dist/index.js \
|
|
98
|
+
--name claude-bridge \
|
|
99
|
+
-- --port 4748 --bind 0.0.0.0 --vps --fixed-token YOUR_TOKEN_HERE
|
|
100
|
+
pm2 save
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Retrieve your token anytime:
|
|
104
|
+
```bash
|
|
105
|
+
cat ~/.claude/ide/*.lock | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8').trim(); console.log(JSON.parse(d).authToken)"
|
|
106
|
+
# or if you have a .env.vps:
|
|
107
|
+
grep FIXED_TOKEN /root/claude-ide-bridge/.env.vps
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Persist across reboots
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Generate and run the startup command PM2 prints
|
|
114
|
+
pm2 startup
|
|
115
|
+
# (run the command it outputs, then:)
|
|
116
|
+
pm2 save
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Using an ecosystem file
|
|
120
|
+
|
|
121
|
+
For repeatable deployments, use the included example:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cp deploy/ecosystem.config.js.example ecosystem.config.js
|
|
125
|
+
# Edit ecosystem.config.js: set cwd, port, and fixed-token
|
|
126
|
+
pm2 start ecosystem.config.js
|
|
127
|
+
pm2 save
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Day-to-day management with PM2
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# View live logs
|
|
134
|
+
pm2 logs claude-bridge
|
|
135
|
+
|
|
136
|
+
# Check status
|
|
137
|
+
pm2 status
|
|
138
|
+
|
|
139
|
+
# Restart after code change
|
|
140
|
+
npm run build && pm2 restart claude-bridge
|
|
141
|
+
|
|
142
|
+
# Stop
|
|
143
|
+
pm2 stop claude-bridge
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## MCP endpoint
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
https://<your-domain>/mcp
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Use in `.mcp.json` for Claude Desktop, claude.ai Custom Connectors, or any remote MCP client:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"claude-ide-bridge": {
|
|
158
|
+
"type": "http",
|
|
159
|
+
"url": "https://your-domain/mcp",
|
|
160
|
+
"headers": {
|
|
161
|
+
"Authorization": "Bearer ${BRIDGE_TOKEN}"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Set `BRIDGE_TOKEN` in your shell profile. Retrieve the token anytime:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
grep FIXED_TOKEN /opt/claude-ide-bridge/.env.vps
|
|
172
|
+
```
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy/bootstrap-new-vps.sh
|
|
3
|
+
# Full fresh-server setup for claude-ide-bridge on a NEW VPS.
|
|
4
|
+
# Handles everything from Node.js install to running HTTPS service.
|
|
5
|
+
#
|
|
6
|
+
# Usage (run as root on a fresh Ubuntu 22.04/24.04 VPS):
|
|
7
|
+
# curl -fsSL https://raw.githubusercontent.com/Oolab-labs/claude-ide-bridge/main/deploy/bootstrap-new-vps.sh | \
|
|
8
|
+
# DOMAIN=bridge.example.com bash
|
|
9
|
+
#
|
|
10
|
+
# Or after cloning:
|
|
11
|
+
# DOMAIN=bridge.example.com bash deploy/bootstrap-new-vps.sh
|
|
12
|
+
#
|
|
13
|
+
# Required environment variables:
|
|
14
|
+
# DOMAIN Subdomain for the bridge (e.g. bridge.example.com)
|
|
15
|
+
#
|
|
16
|
+
# Optional environment variables:
|
|
17
|
+
# REPO_URL Git repo URL (default: https://github.com/Oolab-labs/claude-ide-bridge)
|
|
18
|
+
# INSTALL_DIR Where to clone the repo (default: /opt/claude-ide-bridge)
|
|
19
|
+
# SERVICE_USER System user to run bridge (default: claude-bridge)
|
|
20
|
+
# PORT Bridge port (default: 9000)
|
|
21
|
+
# BRANCH Git branch to clone (default: main)
|
|
22
|
+
# SKIP_CERTBOT Set to 1 to skip TLS cert (useful if DNS not yet set)
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# ── Config ────────────────────────────────────────────────────────────────────
|
|
27
|
+
DOMAIN="${DOMAIN:-}"
|
|
28
|
+
REPO_URL="${REPO_URL:-https://github.com/Oolab-labs/claude-ide-bridge}"
|
|
29
|
+
INSTALL_DIR="${INSTALL_DIR:-/opt/claude-ide-bridge}"
|
|
30
|
+
SERVICE_USER="${SERVICE_USER:-claude-bridge}"
|
|
31
|
+
PORT="${PORT:-9000}"
|
|
32
|
+
BRANCH="${BRANCH:-main}"
|
|
33
|
+
SKIP_CERTBOT="${SKIP_CERTBOT:-0}"
|
|
34
|
+
SERVICE_NAME="claude-ide-bridge"
|
|
35
|
+
|
|
36
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
37
|
+
info() { echo -e "${GREEN}✓${NC} $*"; }
|
|
38
|
+
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
|
39
|
+
err() { echo -e "${RED}✗${NC} $*" >&2; }
|
|
40
|
+
section() { echo ""; echo "── $* ──────────────────────────────────────────"; }
|
|
41
|
+
|
|
42
|
+
# ── Pre-flight ────────────────────────────────────────────────────────────────
|
|
43
|
+
section "Pre-flight"
|
|
44
|
+
|
|
45
|
+
[[ "$(id -u)" -eq 0 ]] || { err "Run as root: sudo bash deploy/bootstrap-new-vps.sh"; exit 1; }
|
|
46
|
+
|
|
47
|
+
if [[ -z "$DOMAIN" ]]; then
|
|
48
|
+
err "DOMAIN is required. Example:"
|
|
49
|
+
echo " DOMAIN=bridge.example.com bash deploy/bootstrap-new-vps.sh"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
info "Domain: $DOMAIN"
|
|
54
|
+
info "Install dir: $INSTALL_DIR"
|
|
55
|
+
info "Service user: $SERVICE_USER"
|
|
56
|
+
info "Port: $PORT"
|
|
57
|
+
info "Branch: $BRANCH"
|
|
58
|
+
|
|
59
|
+
# ── 1. System packages ────────────────────────────────────────────────────────
|
|
60
|
+
section "System packages"
|
|
61
|
+
|
|
62
|
+
apt-get update -qq
|
|
63
|
+
apt-get install -y -qq curl git nginx certbot python3-certbot-nginx ufw
|
|
64
|
+
info "System packages installed"
|
|
65
|
+
|
|
66
|
+
# ── 2. Node.js 20+ ────────────────────────────────────────────────────────────
|
|
67
|
+
section "Node.js"
|
|
68
|
+
|
|
69
|
+
NODE_OK=false
|
|
70
|
+
if command -v node &>/dev/null; then
|
|
71
|
+
NODE_VER=$(node -e 'console.log(parseInt(process.version.slice(1)))')
|
|
72
|
+
[[ "$NODE_VER" -ge 20 ]] && NODE_OK=true
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ "$NODE_OK" == false ]]; then
|
|
76
|
+
info "Installing Node.js 20..."
|
|
77
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
78
|
+
apt-get install -y -qq nodejs
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
NODE_VERSION=$(node --version)
|
|
82
|
+
info "Node $NODE_VERSION / npm $(npm --version)"
|
|
83
|
+
|
|
84
|
+
# ── 3. Service user ───────────────────────────────────────────────────────────
|
|
85
|
+
section "Service user"
|
|
86
|
+
|
|
87
|
+
if ! id "$SERVICE_USER" &>/dev/null; then
|
|
88
|
+
useradd --system --create-home --shell /bin/bash "$SERVICE_USER"
|
|
89
|
+
info "Created user: $SERVICE_USER"
|
|
90
|
+
else
|
|
91
|
+
info "User already exists: $SERVICE_USER"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# ── 4. Clone or update repo ───────────────────────────────────────────────────
|
|
95
|
+
section "Repository"
|
|
96
|
+
|
|
97
|
+
if [[ -d "$INSTALL_DIR/.git" ]]; then
|
|
98
|
+
info "Repo exists — pulling latest..."
|
|
99
|
+
git -C "$INSTALL_DIR" fetch origin
|
|
100
|
+
git -C "$INSTALL_DIR" checkout "$BRANCH"
|
|
101
|
+
git -C "$INSTALL_DIR" pull origin "$BRANCH"
|
|
102
|
+
else
|
|
103
|
+
info "Cloning $REPO_URL..."
|
|
104
|
+
git clone --branch "$BRANCH" --depth 1 "$REPO_URL" "$INSTALL_DIR"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
|
|
108
|
+
info "Repo ready at $INSTALL_DIR"
|
|
109
|
+
|
|
110
|
+
# ── 5. Build ──────────────────────────────────────────────────────────────────
|
|
111
|
+
section "Build"
|
|
112
|
+
|
|
113
|
+
# Run npm install + build as SERVICE_USER
|
|
114
|
+
sudo -u "$SERVICE_USER" bash -c "cd $INSTALL_DIR && npm ci --prefer-offline 2>&1"
|
|
115
|
+
sudo -u "$SERVICE_USER" bash -c "cd $INSTALL_DIR && npm run build 2>&1"
|
|
116
|
+
info "Build complete"
|
|
117
|
+
|
|
118
|
+
# ── 6. Generate .env.vps ──────────────────────────────────────────────────────
|
|
119
|
+
section ".env.vps"
|
|
120
|
+
|
|
121
|
+
ENV_FILE="$INSTALL_DIR/.env.vps"
|
|
122
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
123
|
+
warn ".env.vps already exists — leaving unchanged"
|
|
124
|
+
source "$ENV_FILE"
|
|
125
|
+
info " PORT=$PORT WORKSPACE=$WORKSPACE TOKEN=***"
|
|
126
|
+
else
|
|
127
|
+
FIXED_TOKEN=$(node -e "console.log(require('crypto').randomUUID())")
|
|
128
|
+
WORKSPACE="$INSTALL_DIR"
|
|
129
|
+
cat > "$ENV_FILE" <<EOF
|
|
130
|
+
# Claude IDE Bridge — VPS config
|
|
131
|
+
# Generated by bootstrap-new-vps.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
132
|
+
# Gitignored — never commit this file.
|
|
133
|
+
|
|
134
|
+
PORT=$PORT
|
|
135
|
+
WORKSPACE=$WORKSPACE
|
|
136
|
+
FIXED_TOKEN=$FIXED_TOKEN
|
|
137
|
+
BRIDGE_SESSION=bridge
|
|
138
|
+
NGROK_SESSION=ngrok
|
|
139
|
+
EOF
|
|
140
|
+
chmod 600 "$ENV_FILE"
|
|
141
|
+
chown "$SERVICE_USER:$SERVICE_USER" "$ENV_FILE"
|
|
142
|
+
info ".env.vps created"
|
|
143
|
+
info " PORT=$PORT"
|
|
144
|
+
info " WORKSPACE=$WORKSPACE"
|
|
145
|
+
info " TOKEN=$FIXED_TOKEN"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Reload in case it already existed
|
|
149
|
+
source "$ENV_FILE"
|
|
150
|
+
|
|
151
|
+
# ── 7. Firewall ───────────────────────────────────────────────────────────────
|
|
152
|
+
section "Firewall (ufw)"
|
|
153
|
+
|
|
154
|
+
ufw --force enable >/dev/null 2>&1 || true
|
|
155
|
+
ufw allow ssh >/dev/null 2>&1
|
|
156
|
+
ufw allow 80 >/dev/null 2>&1
|
|
157
|
+
ufw allow 443 >/dev/null 2>&1
|
|
158
|
+
info "ufw: ssh, 80, 443 open"
|
|
159
|
+
|
|
160
|
+
# ── 8. Systemd service ────────────────────────────────────────────────────────
|
|
161
|
+
section "Systemd service"
|
|
162
|
+
|
|
163
|
+
# Generate service file from template (parameterised for this install)
|
|
164
|
+
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<SERVICE
|
|
165
|
+
[Unit]
|
|
166
|
+
Description=Claude IDE Bridge MCP Server
|
|
167
|
+
Documentation=https://github.com/Oolab-labs/claude-ide-bridge
|
|
168
|
+
After=network.target
|
|
169
|
+
StartLimitIntervalSec=120
|
|
170
|
+
StartLimitBurst=5
|
|
171
|
+
|
|
172
|
+
[Service]
|
|
173
|
+
Type=simple
|
|
174
|
+
User=$SERVICE_USER
|
|
175
|
+
WorkingDirectory=$INSTALL_DIR
|
|
176
|
+
|
|
177
|
+
# Load config — FIXED_TOKEN, PORT, WORKSPACE
|
|
178
|
+
EnvironmentFile=$ENV_FILE
|
|
179
|
+
|
|
180
|
+
# Bridge process
|
|
181
|
+
ExecStart=/usr/bin/node $INSTALL_DIR/dist/index.js \\
|
|
182
|
+
--port \${PORT} \\
|
|
183
|
+
--workspace \${WORKSPACE} \\
|
|
184
|
+
--fixed-token \${FIXED_TOKEN} \\
|
|
185
|
+
--grace-period 120000 \\
|
|
186
|
+
--vps
|
|
187
|
+
|
|
188
|
+
KillMode=mixed
|
|
189
|
+
KillSignal=SIGTERM
|
|
190
|
+
TimeoutStopSec=15
|
|
191
|
+
Restart=always
|
|
192
|
+
RestartSec=5
|
|
193
|
+
|
|
194
|
+
StandardOutput=journal
|
|
195
|
+
StandardError=journal
|
|
196
|
+
SyslogIdentifier=claude-ide-bridge
|
|
197
|
+
|
|
198
|
+
NoNewPrivileges=true
|
|
199
|
+
PrivateTmp=true
|
|
200
|
+
|
|
201
|
+
[Install]
|
|
202
|
+
WantedBy=multi-user.target
|
|
203
|
+
SERVICE
|
|
204
|
+
|
|
205
|
+
systemctl daemon-reload
|
|
206
|
+
systemctl enable "$SERVICE_NAME"
|
|
207
|
+
info "Systemd service installed and enabled"
|
|
208
|
+
|
|
209
|
+
# ── 9. nginx ──────────────────────────────────────────────────────────────────
|
|
210
|
+
section "nginx"
|
|
211
|
+
|
|
212
|
+
# Add connection_upgrade map to nginx.conf if missing
|
|
213
|
+
if ! grep -q "connection_upgrade" /etc/nginx/nginx.conf 2>/dev/null; then
|
|
214
|
+
# Insert after the `http {` line
|
|
215
|
+
perl -i -0pe 's/(http \{)/$1\n map \$http_upgrade \$connection_upgrade {\n default upgrade;\n '"''"' close;\n }/' /etc/nginx/nginx.conf
|
|
216
|
+
info "Added connection_upgrade map to nginx.conf"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Write nginx site config (domain injected here, not hardcoded in repo)
|
|
220
|
+
cat > "/etc/nginx/sites-available/claude-bridge" <<NGINX
|
|
221
|
+
# Claude IDE Bridge — nginx reverse proxy
|
|
222
|
+
# Domain: $DOMAIN Port: ${PORT}
|
|
223
|
+
# Generated by bootstrap-new-vps.sh
|
|
224
|
+
|
|
225
|
+
server {
|
|
226
|
+
listen 80;
|
|
227
|
+
listen [::]:80;
|
|
228
|
+
server_name $DOMAIN;
|
|
229
|
+
|
|
230
|
+
location /.well-known/acme-challenge/ {
|
|
231
|
+
root /var/www/html;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
location / {
|
|
235
|
+
return 301 https://\$host\$request_uri;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
server {
|
|
240
|
+
listen 443 ssl http2;
|
|
241
|
+
listen [::]:443 ssl http2;
|
|
242
|
+
server_name $DOMAIN;
|
|
243
|
+
|
|
244
|
+
# TLS — populated by Certbot
|
|
245
|
+
# ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
|
|
246
|
+
# ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
|
|
247
|
+
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
248
|
+
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
249
|
+
|
|
250
|
+
add_header X-Content-Type-Options nosniff always;
|
|
251
|
+
add_header X-Frame-Options DENY always;
|
|
252
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
253
|
+
|
|
254
|
+
# /mcp — Streamable HTTP MCP endpoint (SSE-safe)
|
|
255
|
+
location /mcp {
|
|
256
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
257
|
+
proxy_http_version 1.1;
|
|
258
|
+
proxy_buffering off;
|
|
259
|
+
proxy_cache off;
|
|
260
|
+
proxy_read_timeout 3600s;
|
|
261
|
+
proxy_send_timeout 3600s;
|
|
262
|
+
proxy_connect_timeout 10s;
|
|
263
|
+
proxy_set_header Authorization \$http_authorization;
|
|
264
|
+
proxy_pass_header Authorization;
|
|
265
|
+
proxy_set_header Mcp-Session-Id \$http_mcp_session_id;
|
|
266
|
+
proxy_pass_header Mcp-Session-Id;
|
|
267
|
+
proxy_set_header Host \$host;
|
|
268
|
+
proxy_set_header X-Real-IP \$remote_addr;
|
|
269
|
+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
270
|
+
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
271
|
+
proxy_set_header Upgrade \$http_upgrade;
|
|
272
|
+
proxy_set_header Connection \$connection_upgrade;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
location /health {
|
|
276
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
277
|
+
proxy_http_version 1.1;
|
|
278
|
+
proxy_connect_timeout 5s;
|
|
279
|
+
proxy_read_timeout 10s;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
location /.well-known/ {
|
|
283
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
284
|
+
proxy_http_version 1.1;
|
|
285
|
+
proxy_connect_timeout 5s;
|
|
286
|
+
proxy_read_timeout 10s;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
location / {
|
|
290
|
+
return 404;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
NGINX
|
|
294
|
+
|
|
295
|
+
# Enable site
|
|
296
|
+
[[ -L "/etc/nginx/sites-enabled/claude-bridge" ]] || \
|
|
297
|
+
ln -s /etc/nginx/sites-available/claude-bridge /etc/nginx/sites-enabled/claude-bridge
|
|
298
|
+
|
|
299
|
+
# Remove default site (conflicts on port 80)
|
|
300
|
+
[[ -L "/etc/nginx/sites-enabled/default" ]] && \
|
|
301
|
+
rm -f /etc/nginx/sites-enabled/default && info "Removed nginx default site"
|
|
302
|
+
|
|
303
|
+
nginx -t
|
|
304
|
+
systemctl reload nginx
|
|
305
|
+
info "nginx configured for $DOMAIN"
|
|
306
|
+
|
|
307
|
+
# ── 10. TLS certificate ───────────────────────────────────────────────────────
|
|
308
|
+
section "TLS certificate"
|
|
309
|
+
|
|
310
|
+
if [[ "$SKIP_CERTBOT" == "1" ]]; then
|
|
311
|
+
warn "SKIP_CERTBOT=1 — skipping cert. Run manually later:"
|
|
312
|
+
echo " certbot --nginx -d $DOMAIN"
|
|
313
|
+
else
|
|
314
|
+
if certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos \
|
|
315
|
+
--register-unsafely-without-email --redirect 2>&1; then
|
|
316
|
+
info "TLS certificate obtained for $DOMAIN"
|
|
317
|
+
else
|
|
318
|
+
warn "Certbot failed. Common causes:"
|
|
319
|
+
echo " - DNS not yet pointing to this IP (check: dig $DOMAIN)"
|
|
320
|
+
echo " - Port 80 blocked"
|
|
321
|
+
echo " Re-run manually: certbot --nginx -d $DOMAIN"
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
# ── 11. Start service ─────────────────────────────────────────────────────────
|
|
326
|
+
section "Starting service"
|
|
327
|
+
|
|
328
|
+
systemctl restart "$SERVICE_NAME"
|
|
329
|
+
|
|
330
|
+
echo -n "Waiting for bridge..."
|
|
331
|
+
for i in $(seq 1 30); do
|
|
332
|
+
if curl -sf --max-time 3 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
|
333
|
+
echo " ready."
|
|
334
|
+
break
|
|
335
|
+
fi
|
|
336
|
+
sleep 1; echo -n "."
|
|
337
|
+
if [[ $i -eq 30 ]]; then
|
|
338
|
+
echo " timed out."
|
|
339
|
+
warn "Check logs: journalctl -u $SERVICE_NAME -n 50"
|
|
340
|
+
fi
|
|
341
|
+
done
|
|
342
|
+
|
|
343
|
+
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
344
|
+
echo ""
|
|
345
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
346
|
+
echo " Claude IDE Bridge — bootstrap complete"
|
|
347
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
348
|
+
echo ""
|
|
349
|
+
echo " MCP endpoint: https://$DOMAIN/mcp"
|
|
350
|
+
echo " Health check: https://$DOMAIN/health"
|
|
351
|
+
echo " Local: http://127.0.0.1:${PORT}/mcp"
|
|
352
|
+
echo " Token: ${FIXED_TOKEN}"
|
|
353
|
+
echo " Install dir: $INSTALL_DIR"
|
|
354
|
+
echo " Service user: $SERVICE_USER"
|
|
355
|
+
echo ""
|
|
356
|
+
echo " Service management:"
|
|
357
|
+
echo " systemctl status $SERVICE_NAME"
|
|
358
|
+
echo " journalctl -u $SERVICE_NAME -f"
|
|
359
|
+
echo " systemctl restart $SERVICE_NAME"
|
|
360
|
+
echo ""
|
|
361
|
+
echo " To update:"
|
|
362
|
+
echo " cd $INSTALL_DIR && git pull && npm ci && npm run build"
|
|
363
|
+
echo " systemctl restart $SERVICE_NAME"
|
|
364
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# deploy/claude-ide-bridge.service.template
|
|
2
|
+
# Reference template — paths and user are injected by the install scripts.
|
|
3
|
+
# Do NOT copy this file to /etc/systemd/system/ directly.
|
|
4
|
+
# Use bootstrap-new-vps.sh or install-vps-service.sh which generate
|
|
5
|
+
# the final unit file with all variables substituted.
|
|
6
|
+
|
|
7
|
+
[Unit]
|
|
8
|
+
Description=Claude IDE Bridge MCP Server
|
|
9
|
+
Documentation=https://github.com/Oolab-labs/claude-ide-bridge
|
|
10
|
+
After=network.target
|
|
11
|
+
StartLimitIntervalSec=120
|
|
12
|
+
StartLimitBurst=5
|
|
13
|
+
|
|
14
|
+
[Service]
|
|
15
|
+
Type=simple
|
|
16
|
+
User=${SERVICE_USER}
|
|
17
|
+
WorkingDirectory=${INSTALL_DIR}
|
|
18
|
+
|
|
19
|
+
# Load personal config — FIXED_TOKEN, PORT, WORKSPACE must be set here
|
|
20
|
+
EnvironmentFile=${ENV_FILE}
|
|
21
|
+
|
|
22
|
+
# Bridge process — reads PORT, WORKSPACE, FIXED_TOKEN from EnvironmentFile
|
|
23
|
+
ExecStart=/usr/bin/node ${INSTALL_DIR}/dist/index.js \
|
|
24
|
+
--port ${PORT} \
|
|
25
|
+
--workspace ${WORKSPACE} \
|
|
26
|
+
--fixed-token ${FIXED_TOKEN} \
|
|
27
|
+
--grace-period 120000 \
|
|
28
|
+
--vps
|
|
29
|
+
|
|
30
|
+
# Graceful shutdown: SIGTERM → wait 15s → SIGKILL
|
|
31
|
+
KillMode=mixed
|
|
32
|
+
KillSignal=SIGTERM
|
|
33
|
+
TimeoutStopSec=15
|
|
34
|
+
|
|
35
|
+
# Restart policy: always restart unless manually stopped
|
|
36
|
+
Restart=always
|
|
37
|
+
RestartSec=5
|
|
38
|
+
|
|
39
|
+
# Logging — view with: journalctl -u claude-ide-bridge -f
|
|
40
|
+
StandardOutput=journal
|
|
41
|
+
StandardError=journal
|
|
42
|
+
SyslogIdentifier=claude-ide-bridge
|
|
43
|
+
|
|
44
|
+
# Hardening
|
|
45
|
+
NoNewPrivileges=true
|
|
46
|
+
PrivateTmp=true
|
|
47
|
+
ProtectSystem=strict
|
|
48
|
+
ProtectHome=read-only
|
|
49
|
+
ReadWritePaths=${INSTALL_DIR} /tmp /root/.claude
|
|
50
|
+
CapabilityBoundingSet=
|
|
51
|
+
AmbientCapabilities=
|
|
52
|
+
LockPersonality=true
|
|
53
|
+
RestrictNamespaces=true
|
|
54
|
+
RestrictRealtime=true
|
|
55
|
+
RestrictSUIDSGID=true
|
|
56
|
+
ProtectSystem=strict
|
|
57
|
+
ProtectHome=read-only
|
|
58
|
+
ReadWritePaths=${INSTALL_DIR} /tmp /root/.claude
|
|
59
|
+
CapabilityBoundingSet=
|
|
60
|
+
AmbientCapabilities=
|
|
61
|
+
LockPersonality=true
|
|
62
|
+
RestrictNamespaces=true
|
|
63
|
+
RestrictRealtime=true
|
|
64
|
+
RestrictSUIDSGID=true
|
|
65
|
+
|
|
66
|
+
[Install]
|
|
67
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Claude IDE Bridge MCP Server (for user %i)
|
|
3
|
+
After=network.target
|
|
4
|
+
StartLimitIntervalSec=60
|
|
5
|
+
StartLimitBurst=5
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=%i
|
|
10
|
+
WorkingDirectory=%h
|
|
11
|
+
ExecStart=/usr/local/bin/node /opt/claude-ide-bridge/dist/index.js --workspace %h --jsonl
|
|
12
|
+
Restart=on-failure
|
|
13
|
+
RestartSec=3
|
|
14
|
+
TimeoutStopSec=10
|
|
15
|
+
KillMode=mixed
|
|
16
|
+
KillSignal=SIGTERM
|
|
17
|
+
StandardOutput=journal
|
|
18
|
+
StandardError=journal
|
|
19
|
+
SyslogIdentifier=claude-ide-bridge
|
|
20
|
+
NoNewPrivileges=true
|
|
21
|
+
ProtectSystem=strict
|
|
22
|
+
ReadWritePaths=%h/.claude %h
|
|
23
|
+
PrivateTmp=true
|
|
24
|
+
ProtectHome=read-only
|
|
25
|
+
CapabilityBoundingSet=
|
|
26
|
+
LockPersonality=true
|
|
27
|
+
RestrictNamespaces=true
|
|
28
|
+
RestrictSUIDSGID=true
|
|
29
|
+
|
|
30
|
+
[Install]
|
|
31
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// PM2 ecosystem config for claude-ide-bridge.
|
|
2
|
+
// Copy to the repo root and edit before use:
|
|
3
|
+
// cp deploy/ecosystem.config.js.example ecosystem.config.js
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// pm2 start ecosystem.config.js
|
|
7
|
+
// pm2 save
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT: The --port value below must match the port in your nginx proxy_pass.
|
|
10
|
+
// The bridge defaults to a random port if --port is not set, which breaks nginx.
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
apps: [
|
|
14
|
+
{
|
|
15
|
+
name: "claude-bridge",
|
|
16
|
+
script: "dist/index.js",
|
|
17
|
+
cwd: "/root/claude-ide-bridge", // adjust to your INSTALL_DIR
|
|
18
|
+
|
|
19
|
+
// All bridge flags go here — NOT in env (PORT env var is not read by the bridge)
|
|
20
|
+
args: [
|
|
21
|
+
"--port", "4748", // must match nginx proxy_pass port
|
|
22
|
+
"--bind", "0.0.0.0", // expose to all interfaces (nginx handles TLS)
|
|
23
|
+
"--vps", // expands command allowlist for server use
|
|
24
|
+
"--fixed-token", "YOUR_TOKEN_HERE", // stable token; prevents rotation on restart
|
|
25
|
+
].join(" "),
|
|
26
|
+
|
|
27
|
+
restart_delay: 5000, // wait 5s before restart on crash
|
|
28
|
+
max_restarts: 20, // give up after 20 consecutive crashes
|
|
29
|
+
min_uptime: "10s", // don't count a restart as a crash if up for <10s
|
|
30
|
+
|
|
31
|
+
env: {
|
|
32
|
+
NODE_ENV: "production",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy/install-vps-service.sh
|
|
3
|
+
# Idempotent updater — re-installs the systemd service and nginx config
|
|
4
|
+
# on an already-bootstrapped server after a git pull.
|
|
5
|
+
#
|
|
6
|
+
# For FIRST-TIME setup on a new VPS, use bootstrap-new-vps.sh instead.
|
|
7
|
+
#
|
|
8
|
+
# Usage (run as root from repo root):
|
|
9
|
+
# bash deploy/install-vps-service.sh
|
|
10
|
+
#
|
|
11
|
+
# Optional environment overrides:
|
|
12
|
+
# DOMAIN Override domain (default: read from existing nginx config or prompt)
|
|
13
|
+
# PORT Override port (default: read from .env.vps)
|
|
14
|
+
# SERVICE_USER Override user (default: auto-detect from existing service)
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
19
|
+
SERVICE_NAME="claude-ide-bridge"
|
|
20
|
+
ENV_FILE="$REPO_ROOT/.env.vps"
|
|
21
|
+
|
|
22
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
23
|
+
info() { echo -e "${GREEN}✓${NC} $*"; }
|
|
24
|
+
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
|
25
|
+
err() { echo -e "${RED}✗${NC} $*" >&2; }
|
|
26
|
+
|
|
27
|
+
echo "=== Claude IDE Bridge — Service Updater ==="
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# ── Pre-flight ────────────────────────────────────────────────────────────────
|
|
31
|
+
[[ "$(id -u)" -eq 0 ]] || { err "Run as root: sudo bash deploy/install-vps-service.sh"; exit 1; }
|
|
32
|
+
[[ -f "$ENV_FILE" ]] || { err "$ENV_FILE not found. Run bootstrap-new-vps.sh first."; exit 1; }
|
|
33
|
+
|
|
34
|
+
source "$ENV_FILE"
|
|
35
|
+
[[ -n "${FIXED_TOKEN:-}" ]] || { err "FIXED_TOKEN not set in $ENV_FILE"; exit 1; }
|
|
36
|
+
[[ -n "${PORT:-}" ]] || { err "PORT not set in $ENV_FILE"; exit 1; }
|
|
37
|
+
[[ -n "${WORKSPACE:-}" ]] || { err "WORKSPACE not set in $ENV_FILE"; exit 1; }
|
|
38
|
+
|
|
39
|
+
DIST="$REPO_ROOT/dist/index.js"
|
|
40
|
+
[[ -f "$DIST" ]] || { err "$DIST not found. Run: npm run build"; exit 1; }
|
|
41
|
+
|
|
42
|
+
command -v nginx >/dev/null 2>&1 || { err "nginx not found. Run bootstrap-new-vps.sh first."; exit 1; }
|
|
43
|
+
command -v systemctl >/dev/null 2>&1 || { err "systemd not available."; exit 1; }
|
|
44
|
+
|
|
45
|
+
# Detect service user from existing unit file, fall back to env or default
|
|
46
|
+
EXISTING_USER=$(grep -Po '(?<=^User=).+' /etc/systemd/system/${SERVICE_NAME}.service 2>/dev/null || echo "")
|
|
47
|
+
SERVICE_USER="${SERVICE_USER:-${EXISTING_USER:-claude-bridge}}"
|
|
48
|
+
|
|
49
|
+
# Detect domain from existing nginx config
|
|
50
|
+
EXISTING_DOMAIN=$(grep -Po '(?<=server_name )[\w.-]+' /etc/nginx/sites-available/claude-bridge 2>/dev/null | head -1 || echo "")
|
|
51
|
+
DOMAIN="${DOMAIN:-$EXISTING_DOMAIN}"
|
|
52
|
+
|
|
53
|
+
if [[ -z "$DOMAIN" ]]; then
|
|
54
|
+
err "Cannot determine DOMAIN. Set it: DOMAIN=bridge.example.com bash deploy/install-vps-service.sh"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
info "Repo: $REPO_ROOT"
|
|
59
|
+
info "Domain: $DOMAIN"
|
|
60
|
+
info "Port: $PORT"
|
|
61
|
+
info "Service user: $SERVICE_USER"
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
# ── Systemd service ───────────────────────────────────────────────────────────
|
|
65
|
+
echo "Updating systemd service..."
|
|
66
|
+
|
|
67
|
+
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<SERVICE
|
|
68
|
+
[Unit]
|
|
69
|
+
Description=Claude IDE Bridge MCP Server
|
|
70
|
+
Documentation=https://github.com/Oolab-labs/claude-ide-bridge
|
|
71
|
+
After=network.target
|
|
72
|
+
StartLimitIntervalSec=120
|
|
73
|
+
StartLimitBurst=5
|
|
74
|
+
|
|
75
|
+
[Service]
|
|
76
|
+
Type=simple
|
|
77
|
+
User=$SERVICE_USER
|
|
78
|
+
WorkingDirectory=$REPO_ROOT
|
|
79
|
+
|
|
80
|
+
# Load config — FIXED_TOKEN, PORT, WORKSPACE
|
|
81
|
+
EnvironmentFile=$ENV_FILE
|
|
82
|
+
|
|
83
|
+
# Bridge process
|
|
84
|
+
ExecStart=/usr/bin/node $REPO_ROOT/dist/index.js \\
|
|
85
|
+
--port \${PORT} \\
|
|
86
|
+
--workspace \${WORKSPACE} \\
|
|
87
|
+
--fixed-token \${FIXED_TOKEN} \\
|
|
88
|
+
--grace-period 120000 \\
|
|
89
|
+
--vps
|
|
90
|
+
|
|
91
|
+
KillMode=mixed
|
|
92
|
+
KillSignal=SIGTERM
|
|
93
|
+
TimeoutStopSec=15
|
|
94
|
+
Restart=always
|
|
95
|
+
RestartSec=5
|
|
96
|
+
|
|
97
|
+
StandardOutput=journal
|
|
98
|
+
StandardError=journal
|
|
99
|
+
SyslogIdentifier=claude-ide-bridge
|
|
100
|
+
|
|
101
|
+
NoNewPrivileges=true
|
|
102
|
+
PrivateTmp=true
|
|
103
|
+
|
|
104
|
+
[Install]
|
|
105
|
+
WantedBy=multi-user.target
|
|
106
|
+
SERVICE
|
|
107
|
+
|
|
108
|
+
systemctl daemon-reload
|
|
109
|
+
systemctl enable "$SERVICE_NAME"
|
|
110
|
+
info "Systemd service updated"
|
|
111
|
+
|
|
112
|
+
# ── nginx ─────────────────────────────────────────────────────────────────────
|
|
113
|
+
echo "Updating nginx config..."
|
|
114
|
+
|
|
115
|
+
# Add connection_upgrade map if missing
|
|
116
|
+
if ! grep -q "connection_upgrade" /etc/nginx/nginx.conf 2>/dev/null; then
|
|
117
|
+
perl -i -0pe 's/(http \{)/$1\n map \$http_upgrade \$connection_upgrade {\n default upgrade;\n '"''"' close;\n }/' /etc/nginx/nginx.conf
|
|
118
|
+
info "Added connection_upgrade map to nginx.conf"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Write site config with domain and port injected
|
|
122
|
+
cat > "/etc/nginx/sites-available/claude-bridge" <<NGINX
|
|
123
|
+
# Claude IDE Bridge — nginx reverse proxy
|
|
124
|
+
# Domain: $DOMAIN Port: ${PORT}
|
|
125
|
+
# Updated by install-vps-service.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
126
|
+
|
|
127
|
+
server {
|
|
128
|
+
listen 80;
|
|
129
|
+
listen [::]:80;
|
|
130
|
+
server_name $DOMAIN;
|
|
131
|
+
|
|
132
|
+
location /.well-known/acme-challenge/ {
|
|
133
|
+
root /var/www/html;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
location / {
|
|
137
|
+
return 301 https://\$host\$request_uri;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
server {
|
|
142
|
+
listen 443 ssl http2;
|
|
143
|
+
listen [::]:443 ssl http2;
|
|
144
|
+
server_name $DOMAIN;
|
|
145
|
+
|
|
146
|
+
# TLS — managed by Certbot
|
|
147
|
+
# ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
|
|
148
|
+
# ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
|
|
149
|
+
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
150
|
+
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
151
|
+
|
|
152
|
+
add_header X-Content-Type-Options nosniff always;
|
|
153
|
+
add_header X-Frame-Options DENY always;
|
|
154
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
155
|
+
|
|
156
|
+
location /mcp {
|
|
157
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
158
|
+
proxy_http_version 1.1;
|
|
159
|
+
proxy_buffering off;
|
|
160
|
+
proxy_cache off;
|
|
161
|
+
proxy_read_timeout 86400s;
|
|
162
|
+
proxy_send_timeout 86400s;
|
|
163
|
+
proxy_connect_timeout 10s;
|
|
164
|
+
proxy_set_header Authorization \$http_authorization;
|
|
165
|
+
proxy_pass_header Authorization;
|
|
166
|
+
proxy_set_header Mcp-Session-Id \$http_mcp_session_id;
|
|
167
|
+
proxy_pass_header Mcp-Session-Id;
|
|
168
|
+
proxy_set_header Host \$host;
|
|
169
|
+
proxy_set_header X-Real-IP \$remote_addr;
|
|
170
|
+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
171
|
+
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
172
|
+
proxy_set_header Upgrade \$http_upgrade;
|
|
173
|
+
proxy_set_header Connection \$connection_upgrade;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
location /health {
|
|
177
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
178
|
+
proxy_http_version 1.1;
|
|
179
|
+
proxy_connect_timeout 5s;
|
|
180
|
+
proxy_read_timeout 10s;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
location /.well-known/ {
|
|
184
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
185
|
+
proxy_http_version 1.1;
|
|
186
|
+
proxy_connect_timeout 5s;
|
|
187
|
+
proxy_read_timeout 10s;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
location / {
|
|
191
|
+
return 404;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
NGINX
|
|
195
|
+
|
|
196
|
+
[[ -L "/etc/nginx/sites-enabled/claude-bridge" ]] || \
|
|
197
|
+
ln -s /etc/nginx/sites-available/claude-bridge /etc/nginx/sites-enabled/claude-bridge
|
|
198
|
+
|
|
199
|
+
[[ -L "/etc/nginx/sites-enabled/default" ]] && \
|
|
200
|
+
rm -f /etc/nginx/sites-enabled/default
|
|
201
|
+
|
|
202
|
+
nginx -t
|
|
203
|
+
systemctl reload nginx
|
|
204
|
+
info "nginx config updated"
|
|
205
|
+
|
|
206
|
+
# ── Stop tmux session if still running (superseded by systemd) ────────────────
|
|
207
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "bridge" 2>/dev/null; then
|
|
208
|
+
tmux kill-session -t "bridge" 2>/dev/null || true
|
|
209
|
+
info "Stopped legacy tmux bridge session"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# ── Restart service ───────────────────────────────────────────────────────────
|
|
213
|
+
echo "Restarting service..."
|
|
214
|
+
systemctl restart "$SERVICE_NAME"
|
|
215
|
+
|
|
216
|
+
echo -n "Waiting for bridge..."
|
|
217
|
+
for i in $(seq 1 20); do
|
|
218
|
+
if curl -sf --max-time 3 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
|
219
|
+
echo " ready."
|
|
220
|
+
break
|
|
221
|
+
fi
|
|
222
|
+
sleep 1; echo -n "."
|
|
223
|
+
if [[ $i -eq 20 ]]; then
|
|
224
|
+
echo " timed out."
|
|
225
|
+
warn "Check: journalctl -u $SERVICE_NAME -n 50"
|
|
226
|
+
fi
|
|
227
|
+
done
|
|
228
|
+
|
|
229
|
+
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
230
|
+
echo ""
|
|
231
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
232
|
+
echo " Claude IDE Bridge — service updated"
|
|
233
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
234
|
+
echo ""
|
|
235
|
+
echo " MCP endpoint: https://$DOMAIN/mcp"
|
|
236
|
+
echo " Local: http://127.0.0.1:${PORT}/mcp"
|
|
237
|
+
echo " Token: ${FIXED_TOKEN}"
|
|
238
|
+
echo ""
|
|
239
|
+
echo " journalctl -u $SERVICE_NAME -f"
|
|
240
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# deploy/nginx-claude-bridge.conf.template
|
|
2
|
+
# Reference template — domain and port are injected by the install scripts.
|
|
3
|
+
# Do NOT use this file directly. Use bootstrap-new-vps.sh or install-vps-service.sh
|
|
4
|
+
# which write the final nginx config to /etc/nginx/sites-available/claude-bridge
|
|
5
|
+
# with $DOMAIN and $PORT substituted.
|
|
6
|
+
#
|
|
7
|
+
# To manually generate from this template:
|
|
8
|
+
# DOMAIN=bridge.example.com PORT=9000 \
|
|
9
|
+
# envsubst '$DOMAIN $PORT' < deploy/nginx-claude-bridge.conf.template \
|
|
10
|
+
# > /etc/nginx/sites-available/claude-bridge
|
|
11
|
+
|
|
12
|
+
server {
|
|
13
|
+
listen 80;
|
|
14
|
+
listen [::]:80;
|
|
15
|
+
server_name ${DOMAIN};
|
|
16
|
+
|
|
17
|
+
# Let Certbot complete ACME HTTP-01 challenges
|
|
18
|
+
location /.well-known/acme-challenge/ {
|
|
19
|
+
root /var/www/html;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Redirect all other HTTP to HTTPS
|
|
23
|
+
location / {
|
|
24
|
+
return 301 https://$host$request_uri;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
server {
|
|
29
|
+
listen 443 ssl http2;
|
|
30
|
+
listen [::]:443 ssl http2;
|
|
31
|
+
server_name ${DOMAIN};
|
|
32
|
+
|
|
33
|
+
# TLS — Certbot populates these after: certbot --nginx -d ${DOMAIN}
|
|
34
|
+
# ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
|
35
|
+
# ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
|
36
|
+
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
37
|
+
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
38
|
+
|
|
39
|
+
# Security headers
|
|
40
|
+
add_header X-Content-Type-Options nosniff always;
|
|
41
|
+
add_header X-Frame-Options DENY always;
|
|
42
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
43
|
+
add_header Content-Security-Policy "default-src 'none'" always;
|
|
44
|
+
|
|
45
|
+
# /mcp — Streamable HTTP MCP endpoint (SSE-safe, never buffer)
|
|
46
|
+
location /mcp {
|
|
47
|
+
# Rate limit: 30 req/min per IP, burst of 10 (requires limit_req_zone in http{} block)
|
|
48
|
+
limit_req zone=mcp_zone burst=10 nodelay;
|
|
49
|
+
limit_req_status 429;
|
|
50
|
+
|
|
51
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
52
|
+
proxy_http_version 1.1;
|
|
53
|
+
|
|
54
|
+
# SSE requires no buffering
|
|
55
|
+
proxy_buffering off;
|
|
56
|
+
proxy_cache off;
|
|
57
|
+
proxy_read_timeout 86400s;
|
|
58
|
+
proxy_send_timeout 86400s;
|
|
59
|
+
proxy_connect_timeout 10s;
|
|
60
|
+
|
|
61
|
+
# Forward auth and MCP session headers
|
|
62
|
+
proxy_set_header Authorization $http_authorization;
|
|
63
|
+
proxy_pass_header Authorization;
|
|
64
|
+
proxy_set_header Mcp-Session-Id $http_mcp_session_id;
|
|
65
|
+
proxy_pass_header Mcp-Session-Id;
|
|
66
|
+
|
|
67
|
+
# Standard proxy headers
|
|
68
|
+
proxy_set_header Host $host;
|
|
69
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
70
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
71
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
72
|
+
|
|
73
|
+
# WebSocket upgrade passthrough
|
|
74
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
75
|
+
proxy_set_header Connection $connection_upgrade;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# /health — unauthenticated health check
|
|
79
|
+
location /health {
|
|
80
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
81
|
+
proxy_http_version 1.1;
|
|
82
|
+
proxy_connect_timeout 5s;
|
|
83
|
+
proxy_read_timeout 10s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# /.well-known — MCP server card + OAuth discovery (RFC 8414, RFC 9396)
|
|
87
|
+
location /.well-known/ {
|
|
88
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
89
|
+
proxy_http_version 1.1;
|
|
90
|
+
proxy_connect_timeout 5s;
|
|
91
|
+
proxy_read_timeout 10s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# /oauth — OAuth 2.0 endpoints (authorize, token, register, revoke)
|
|
95
|
+
location /oauth/ {
|
|
96
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
97
|
+
proxy_http_version 1.1;
|
|
98
|
+
proxy_connect_timeout 5s;
|
|
99
|
+
proxy_read_timeout 30s;
|
|
100
|
+
proxy_set_header Host $host;
|
|
101
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
102
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
103
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# /ping — unauthenticated health probe
|
|
107
|
+
location /ping {
|
|
108
|
+
proxy_pass http://127.0.0.1:${PORT};
|
|
109
|
+
proxy_http_version 1.1;
|
|
110
|
+
proxy_connect_timeout 5s;
|
|
111
|
+
proxy_read_timeout 10s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Block everything else
|
|
115
|
+
location / {
|
|
116
|
+
return 404;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Note: add this to the `http {}` block in /etc/nginx/nginx.conf if not present
|
|
121
|
+
# (bootstrap-new-vps.sh and install-vps-service.sh do this automatically):
|
|
122
|
+
#
|
|
123
|
+
# map $http_upgrade $connection_upgrade {
|
|
124
|
+
# default upgrade;
|
|
125
|
+
# '' close;
|
|
126
|
+
# }
|
|
127
|
+
#
|
|
128
|
+
# Rate limiting zone — add to http {} block:
|
|
129
|
+
# limit_req_zone $binary_remote_addr zone=mcp_zone:10m rate=30r/m;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ide-bridge",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.8",
|
|
4
4
|
"description": "Standalone MCP bridge for Claude Code IDE integration with any editor — 136+ tools for LSP, debugging, terminals, Git, GitHub, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
"scripts/mcp-stdio-shim.cjs",
|
|
55
55
|
"scripts/postinstall.mjs",
|
|
56
56
|
"scripts/start-vps.sh",
|
|
57
|
+
"scripts/gen-claude-desktop-config.sh",
|
|
57
58
|
"templates",
|
|
59
|
+
"deploy",
|
|
58
60
|
"LICENSE",
|
|
59
61
|
"README.md"
|
|
60
62
|
],
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"node": ">=20.0.0"
|
|
63
65
|
},
|
|
64
66
|
"scripts": {
|
|
65
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node scripts/postinstall.mjs",
|
|
67
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && (node scripts/postinstall.mjs || true)",
|
|
66
68
|
"dev": "tsx src/index.ts",
|
|
67
69
|
"start": "node dist/index.js",
|
|
68
70
|
"test": "vitest run",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generate or update claude_desktop_config.json so Claude Desktop connects
|
|
3
|
+
# to the running claude-ide-bridge via the stdio shim.
|
|
4
|
+
#
|
|
5
|
+
# Run with `bash`, do not source this script.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/gen-claude-desktop-config.sh # Print config
|
|
9
|
+
# bash scripts/gen-claude-desktop-config.sh --write # Write to config file
|
|
10
|
+
#
|
|
11
|
+
# The shim auto-discovers the running bridge via lock files in ~/.claude/ide/.
|
|
12
|
+
# No port or token needs to be hard-coded -- just start the bridge first.
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
SHIM_PATH="${SCRIPT_DIR}/mcp-stdio-shim.cjs"
|
|
18
|
+
|
|
19
|
+
# --- Detect config path ---
|
|
20
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
21
|
+
CONFIG_PATH="${HOME}/Library/Application Support/Claude/claude_desktop_config.json"
|
|
22
|
+
else
|
|
23
|
+
CONFIG_PATH="${APPDATA:-${HOME}/.config}/Claude/claude_desktop_config.json"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
WRITE=false
|
|
27
|
+
for arg in "$@"; do
|
|
28
|
+
case "$arg" in
|
|
29
|
+
--write) WRITE=true ;;
|
|
30
|
+
--help|-h)
|
|
31
|
+
echo "Usage: $0 [--write]"
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Generates a claude_desktop_config.json entry so Claude Desktop"
|
|
34
|
+
echo "connects to the running claude-ide-bridge via the stdio shim."
|
|
35
|
+
echo ""
|
|
36
|
+
echo " --write Merge into ${CONFIG_PATH}"
|
|
37
|
+
echo " (backs up existing config first)"
|
|
38
|
+
exit 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# --- Verify shim exists ---
|
|
44
|
+
if [[ ! -f "$SHIM_PATH" ]]; then
|
|
45
|
+
echo "Error: stdio shim not found at $SHIM_PATH" >&2
|
|
46
|
+
echo "Make sure you're running this from the bridge repo." >&2
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# --- Verify bridge is running (optional, warn only) ---
|
|
51
|
+
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
52
|
+
LOCK_DIR="$CLAUDE_DIR/ide"
|
|
53
|
+
LOCK_COUNT=$(find "$LOCK_DIR" -maxdepth 1 -name "*.lock" 2>/dev/null | wc -l | tr -d ' ')
|
|
54
|
+
if [[ "$LOCK_COUNT" -eq 0 ]]; then
|
|
55
|
+
echo "Warning: No bridge lock files found. Start the bridge first (npm run start-all)." >&2
|
|
56
|
+
echo "The config will still be generated -- the shim will connect when the bridge starts." >&2
|
|
57
|
+
echo ""
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- Build or merge config ---
|
|
61
|
+
# Pass SHIM_PATH via env var to avoid path injection in python -c string
|
|
62
|
+
if [[ -f "$CONFIG_PATH" ]]; then
|
|
63
|
+
MERGED=$(SHIM_PATH="$SHIM_PATH" CONFIG_PATH="$CONFIG_PATH" python3 - <<'PYEOF'
|
|
64
|
+
import json, os, sys
|
|
65
|
+
shim = os.environ["SHIM_PATH"]
|
|
66
|
+
config_path = os.environ["CONFIG_PATH"]
|
|
67
|
+
try:
|
|
68
|
+
with open(config_path, "r") as f:
|
|
69
|
+
config = json.load(f)
|
|
70
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
71
|
+
config = {}
|
|
72
|
+
if "mcpServers" not in config:
|
|
73
|
+
config["mcpServers"] = {}
|
|
74
|
+
config["mcpServers"]["claude-ide-bridge"] = {
|
|
75
|
+
"command": "node",
|
|
76
|
+
"args": [shim]
|
|
77
|
+
}
|
|
78
|
+
print(json.dumps(config, indent=2))
|
|
79
|
+
PYEOF
|
|
80
|
+
)
|
|
81
|
+
else
|
|
82
|
+
MERGED=$(SHIM_PATH="$SHIM_PATH" python3 - <<'PYEOF'
|
|
83
|
+
import json, os
|
|
84
|
+
shim = os.environ["SHIM_PATH"]
|
|
85
|
+
config = {
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"claude-ide-bridge": {
|
|
88
|
+
"command": "node",
|
|
89
|
+
"args": [shim]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
print(json.dumps(config, indent=2))
|
|
94
|
+
PYEOF
|
|
95
|
+
)
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo "=== Claude Desktop MCP Config ==="
|
|
99
|
+
echo "Config path: $CONFIG_PATH"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "$MERGED"
|
|
102
|
+
|
|
103
|
+
if $WRITE; then
|
|
104
|
+
echo ""
|
|
105
|
+
# Atomic write: write to .tmp then mv to prevent partial writes
|
|
106
|
+
TMP_PATH="${CONFIG_PATH}.tmp"
|
|
107
|
+
mkdir -p "$(dirname "$CONFIG_PATH")"
|
|
108
|
+
# Back up existing config with timestamp to avoid clobbering previous backups
|
|
109
|
+
if [[ -f "$CONFIG_PATH" ]]; then
|
|
110
|
+
BACKUP="${CONFIG_PATH}.$(date +%Y%m%d%H%M%S).bak"
|
|
111
|
+
cp "$CONFIG_PATH" "$BACKUP"
|
|
112
|
+
echo "Backed up existing config to $(basename "$BACKUP")"
|
|
113
|
+
fi
|
|
114
|
+
printf '%s\n' "$MERGED" > "$TMP_PATH"
|
|
115
|
+
mv "$TMP_PATH" "$CONFIG_PATH"
|
|
116
|
+
echo "Written: $CONFIG_PATH"
|
|
117
|
+
echo ""
|
|
118
|
+
echo "Restart Claude Desktop to pick up the new config."
|
|
119
|
+
echo "Then ask Claude: \"What files are open in my IDE?\""
|
|
120
|
+
else
|
|
121
|
+
echo ""
|
|
122
|
+
echo "Run with --write to save this config, or copy it manually to:"
|
|
123
|
+
echo " $CONFIG_PATH"
|
|
124
|
+
fi
|