@aiwerk/mcp-bridge 1.0.0

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/bin/mcp-bridge.js +9 -0
  4. package/bin/mcp-bridge.ts +335 -0
  5. package/package.json +42 -0
  6. package/scripts/install-server.ps1 +300 -0
  7. package/scripts/install-server.sh +357 -0
  8. package/servers/apify/README.md +40 -0
  9. package/servers/apify/config.json +13 -0
  10. package/servers/apify/env_vars +1 -0
  11. package/servers/apify/install.ps1 +3 -0
  12. package/servers/apify/install.sh +4 -0
  13. package/servers/candidates.md +13 -0
  14. package/servers/github/README.md +40 -0
  15. package/servers/github/config.json +21 -0
  16. package/servers/github/env_vars +1 -0
  17. package/servers/github/install.ps1 +3 -0
  18. package/servers/github/install.sh +4 -0
  19. package/servers/google-maps/README.md +40 -0
  20. package/servers/google-maps/config.json +17 -0
  21. package/servers/google-maps/env_vars +1 -0
  22. package/servers/google-maps/install.ps1 +3 -0
  23. package/servers/google-maps/install.sh +4 -0
  24. package/servers/hetzner/README.md +41 -0
  25. package/servers/hetzner/config.json +16 -0
  26. package/servers/hetzner/env_vars +1 -0
  27. package/servers/hetzner/install.ps1 +3 -0
  28. package/servers/hetzner/install.sh +4 -0
  29. package/servers/hostinger/README.md +40 -0
  30. package/servers/hostinger/config.json +17 -0
  31. package/servers/hostinger/env_vars +1 -0
  32. package/servers/hostinger/install.ps1 +3 -0
  33. package/servers/hostinger/install.sh +4 -0
  34. package/servers/index.json +125 -0
  35. package/servers/linear/README.md +40 -0
  36. package/servers/linear/config.json +16 -0
  37. package/servers/linear/env_vars +1 -0
  38. package/servers/linear/install.ps1 +3 -0
  39. package/servers/linear/install.sh +4 -0
  40. package/servers/miro/README.md +40 -0
  41. package/servers/miro/config.json +19 -0
  42. package/servers/miro/env_vars +1 -0
  43. package/servers/miro/install.ps1 +3 -0
  44. package/servers/miro/install.sh +4 -0
  45. package/servers/notion/README.md +42 -0
  46. package/servers/notion/config.json +17 -0
  47. package/servers/notion/env_vars +1 -0
  48. package/servers/notion/install.ps1 +3 -0
  49. package/servers/notion/install.sh +4 -0
  50. package/servers/stripe/README.md +40 -0
  51. package/servers/stripe/config.json +19 -0
  52. package/servers/stripe/env_vars +1 -0
  53. package/servers/stripe/install.ps1 +3 -0
  54. package/servers/stripe/install.sh +4 -0
  55. package/servers/tavily/README.md +40 -0
  56. package/servers/tavily/config.json +17 -0
  57. package/servers/tavily/env_vars +1 -0
  58. package/servers/tavily/install.ps1 +3 -0
  59. package/servers/tavily/install.sh +4 -0
  60. package/servers/todoist/README.md +40 -0
  61. package/servers/todoist/config.json +17 -0
  62. package/servers/todoist/env_vars +1 -0
  63. package/servers/todoist/install.ps1 +3 -0
  64. package/servers/todoist/install.sh +4 -0
  65. package/servers/wise/README.md +41 -0
  66. package/servers/wise/config.json +16 -0
  67. package/servers/wise/env_vars +1 -0
  68. package/servers/wise/install.ps1 +3 -0
  69. package/servers/wise/install.sh +4 -0
  70. package/src/config.ts +168 -0
  71. package/src/index.ts +44 -0
  72. package/src/mcp-router.ts +366 -0
  73. package/src/protocol.ts +69 -0
  74. package/src/schema-convert.ts +178 -0
  75. package/src/standalone-server.ts +385 -0
  76. package/src/tool-naming.ts +51 -0
  77. package/src/transport-base.ts +199 -0
  78. package/src/transport-sse.ts +230 -0
  79. package/src/transport-stdio.ts +312 -0
  80. package/src/transport-streamable-http.ts +188 -0
  81. package/src/types.ts +88 -0
  82. package/src/update-checker.ts +155 -0
  83. package/tests/collision.test.ts +60 -0
  84. package/tests/env-resolve.test.ts +68 -0
  85. package/tests/mcp-router.test.ts +301 -0
  86. package/tests/schema-convert.test.ts +70 -0
  87. package/tests/transport-base.test.ts +214 -0
  88. package/tsconfig.json +15 -0
@@ -0,0 +1,300 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
4
+ $OpenclawDir = Join-Path $env:USERPROFILE ".openclaw"
5
+ $EnvFile = Join-Path $OpenclawDir ".env"
6
+ $OpenclawJson = Join-Path $OpenclawDir "openclaw.json"
7
+
8
+ if ($args.Count -eq 0) {
9
+ Write-Host "Usage: install-server.ps1 <server-name> [--dry-run] [--remove]"
10
+ Write-Host ""
11
+ Write-Host "Available servers:"
12
+ Get-ChildItem -Path (Join-Path $ScriptDir "servers") -Directory | ForEach-Object { Write-Host " - $($_.Name)" }
13
+ exit 1
14
+ }
15
+
16
+ $ServerName = $args[0]
17
+ $DryRun = $args -contains "--dry-run"
18
+ $Remove = $args -contains "--remove"
19
+
20
+ $ServerDir = Join-Path $ScriptDir "servers\$ServerName"
21
+ if (-not (Test-Path $ServerDir)) {
22
+ Write-Host "Error: Server '$ServerName' not found."
23
+ Get-ChildItem -Path (Join-Path $ScriptDir "servers") -Directory | ForEach-Object { Write-Host " - $($_.Name)" }
24
+ exit 1
25
+ }
26
+
27
+ $ServerTitle = ($ServerName -replace '-', ' ' -split ' ' | ForEach-Object { if ($_.Length -gt 0) { $_.Substring(0,1).ToUpper() + $_.Substring(1) } }) -join ' '
28
+ $ServerConfigFile = Join-Path $ServerDir "config.json"
29
+ $EnvVarsFile = Join-Path $ServerDir "env_vars"
30
+
31
+ function Require-Command {
32
+ param([string]$Name)
33
+ if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
34
+ throw "Missing required command: $Name"
35
+ }
36
+ }
37
+
38
+ function Get-TokenUrl {
39
+ switch ($ServerName) {
40
+ "apify" { "https://console.apify.com/settings/integrations" }
41
+ "github" { "https://github.com/settings/tokens" }
42
+ "google-maps" { "https://console.cloud.google.com/apis/credentials" }
43
+ "hetzner" { "https://console.hetzner.cloud/" }
44
+ "hostinger" { "https://hpanel.hostinger.com/api" }
45
+ "linear" { "https://linear.app/settings/api" }
46
+ "miro" { "https://miro.com/app/settings/user-profile/apps" }
47
+ "notion" { "https://www.notion.so/my-integrations" }
48
+ "stripe" { "https://dashboard.stripe.com/apikeys" }
49
+ "tavily" { "https://app.tavily.com/home" }
50
+ "todoist" { "https://app.todoist.com/app/settings/integrations/developer" }
51
+ "wise" { "https://wise.com/settings/api-tokens" }
52
+ default { "" }
53
+ }
54
+ }
55
+
56
+ function Check-Prerequisites {
57
+ switch ($ServerName) {
58
+ "github" { Require-Command docker }
59
+ "linear" { Require-Command node; Require-Command npm }
60
+ { $_ -in "wise","hetzner" } { Require-Command git; Require-Command node; Require-Command npm }
61
+ default { Require-Command node; Require-Command npx }
62
+ }
63
+ }
64
+
65
+ function Install-Dependencies {
66
+ switch ($ServerName) {
67
+ "github" {
68
+ Write-Host "Pulling GitHub MCP server Docker image..."
69
+ docker pull ghcr.io/github/github-mcp-server | Out-Host
70
+ }
71
+ "linear" {
72
+ Write-Host "Installing @anthropic-pb/linear-mcp-server globally..."
73
+ npm install -g @anthropic-pb/linear-mcp-server | Out-Host
74
+ }
75
+ "wise" {
76
+ $cloneDir = Join-Path $ServerDir "mcp-server"
77
+ if (Test-Path (Join-Path $cloneDir ".git")) {
78
+ Write-Host "Updating wise mcp-server..."
79
+ git -C $cloneDir pull --ff-only | Out-Host
80
+ } else {
81
+ Write-Host "Cloning wise mcp-server..."
82
+ git clone https://github.com/Szotasz/wise-mcp.git $cloneDir | Out-Host
83
+ }
84
+ Push-Location $cloneDir
85
+ npm install | Out-Host; npm run build | Out-Host
86
+ Pop-Location
87
+ }
88
+ "hetzner" {
89
+ $cloneDir = Join-Path $ServerDir "mcp-server"
90
+ if (Test-Path (Join-Path $cloneDir ".git")) {
91
+ Write-Host "Updating hetzner mcp-server..."
92
+ git -C $cloneDir pull --ff-only | Out-Host
93
+ } else {
94
+ Write-Host "Cloning hetzner mcp-server..."
95
+ git clone https://github.com/dkruyt/mcp-hetzner.git $cloneDir | Out-Host
96
+ }
97
+ Push-Location $cloneDir
98
+ npm install | Out-Host; npm run build | Out-Host
99
+ Pop-Location
100
+ }
101
+ }
102
+ }
103
+
104
+ function Get-PathOverride {
105
+ switch ($ServerName) {
106
+ "linear" {
107
+ $npmRoot = npm root -g
108
+ $distPath = Join-Path $npmRoot "@anthropic-pb/linear-mcp-server\dist\index.js"
109
+ $buildPath = Join-Path $npmRoot "@anthropic-pb/linear-mcp-server\build\index.js"
110
+ if (Test-Path $distPath) { return $distPath }
111
+ if (Test-Path $buildPath) { return $buildPath }
112
+ return $distPath
113
+ }
114
+ "wise" { return (Join-Path $ServerDir "mcp-server\dist\cli.js") }
115
+ "hetzner" { return (Join-Path $ServerDir "mcp-server\dist\index.js") }
116
+ default { return "" }
117
+ }
118
+ }
119
+
120
+ function Ensure-Property {
121
+ param($Object, [string]$Name, $DefaultValue)
122
+ if (-not ($Object.PSObject.Properties.Name -contains $Name)) {
123
+ $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $DefaultValue
124
+ }
125
+ return $Object.$Name
126
+ }
127
+
128
+ # ========================================
129
+ # REMOVE MODE
130
+ # ========================================
131
+ if ($Remove) {
132
+ Write-Host "========================================"
133
+ Write-Host "Removing $ServerTitle MCP Server"
134
+ Write-Host "========================================"
135
+
136
+ if (-not (Test-Path $OpenclawJson)) {
137
+ Write-Host "❌ Config not found: $OpenclawJson" -ForegroundColor Red
138
+ exit 1
139
+ }
140
+
141
+ $cfg = Get-Content $OpenclawJson -Raw | ConvertFrom-Json
142
+ $servers = $cfg.plugins.entries.'openclaw-mcp-bridge'.config.servers
143
+ if (-not ($servers.PSObject.Properties.Name -contains $ServerName)) {
144
+ Write-Host "ℹ️ Server '$ServerName' not found in config. Nothing to remove." -ForegroundColor Yellow
145
+ exit 0
146
+ }
147
+
148
+ # Backup
149
+ $backupFile = "$OpenclawJson.bak-$(Get-Date -Format 'yyyyMMddHHmmss')"
150
+ Copy-Item $OpenclawJson $backupFile
151
+ Write-Host "Backup: $backupFile"
152
+
153
+ # Remove server entry
154
+ $servers.PSObject.Properties.Remove($ServerName)
155
+ $cfg | ConvertTo-Json -Depth 10 | Set-Content $OpenclawJson -Encoding UTF8
156
+ Write-Host "✅ Removed $ServerName from config" -ForegroundColor Green
157
+ Write-Host "ℹ️ Server recipe kept in servers\$ServerName\ (reinstall anytime)" -ForegroundColor Cyan
158
+
159
+ # Remove env var from .env
160
+ $envVarsFile = Join-Path $ServerDir "env_vars"
161
+ if ((Test-Path $envVarsFile) -and (Test-Path $EnvFile)) {
162
+ $envVarName = (Get-Content $envVarsFile -TotalCount 1).Trim()
163
+ $envContent = Get-Content $EnvFile
164
+ $filtered = $envContent | Where-Object { $_ -notmatch "^$envVarName=" }
165
+ if ($filtered.Count -lt $envContent.Count) {
166
+ $filtered | Set-Content $EnvFile -Encoding UTF8
167
+ Write-Host "🔑 Removed $envVarName from $EnvFile" -ForegroundColor Green
168
+ }
169
+ }
170
+
171
+ $restart = Read-Host "Restart gateway now? [Y/n]"
172
+ if ([string]::IsNullOrEmpty($restart) -or $restart -match '^[Yy]$') {
173
+ try {
174
+ Restart-Service openclaw-gateway -ErrorAction Stop
175
+ Write-Host "✅ Gateway restarted. $ServerTitle removed." -ForegroundColor Green
176
+ } catch {
177
+ Write-Host "⚠️ Auto-restart failed. Run: Restart-Service openclaw-gateway" -ForegroundColor Yellow
178
+ }
179
+ } else {
180
+ Write-Host "⏭️ Run manually: Restart-Service openclaw-gateway"
181
+ }
182
+ exit 0
183
+ }
184
+
185
+ # ========================================
186
+ # INSTALL MODE
187
+ # ========================================
188
+ Write-Host "========================================"
189
+ Write-Host "Installing $ServerTitle MCP Server"
190
+ Write-Host "========================================"
191
+
192
+ if ($DryRun) {
193
+ Write-Host "[DRY RUN] Server: $ServerName"
194
+ if (Test-Path $EnvVarsFile) { Write-Host "[DRY RUN] Env var: $(Get-Content $EnvVarsFile -TotalCount 1)" }
195
+ Write-Host "[DRY RUN] Config:"; Get-Content $ServerConfigFile
196
+ exit 0
197
+ }
198
+
199
+ if (-not (Test-Path $EnvVarsFile)) { throw "Missing env_vars file in $ServerDir" }
200
+
201
+ $EnvVarName = (Get-Content $EnvVarsFile -TotalCount 1).Trim()
202
+ if ([string]::IsNullOrWhiteSpace($EnvVarName)) { throw "env_vars file does not contain a variable name" }
203
+
204
+ # 1. Prerequisites
205
+ Check-Prerequisites
206
+
207
+ # 2. Dependencies
208
+ Install-Dependencies
209
+
210
+ # 3. Get API token
211
+ $tokenUrl = Get-TokenUrl
212
+ if ($tokenUrl) { Write-Host "Get your API token here: $tokenUrl" }
213
+
214
+ $Token = ""
215
+ while ([string]::IsNullOrWhiteSpace($Token)) {
216
+ $Token = Read-Host "Enter your $ServerTitle API token"
217
+ if ([string]::IsNullOrWhiteSpace($Token)) { Write-Host "Token cannot be empty." }
218
+ }
219
+
220
+ # 4. Write to .env
221
+ New-Item -ItemType Directory -Force -Path $OpenclawDir | Out-Null
222
+ if (-not (Test-Path $EnvFile)) { New-Item -ItemType File -Force -Path $EnvFile | Out-Null }
223
+
224
+ $envExists = Select-String -Path $EnvFile -Pattern "^$([regex]::Escape($EnvVarName))=" -Quiet
225
+ if ($envExists) {
226
+ $overwrite = Read-Host "$EnvVarName already exists. Overwrite with new token? [y/N]"
227
+ if ($overwrite -match "^[Yy]$") {
228
+ $content = Get-Content $EnvFile | Where-Object { $_ -notmatch "^$([regex]::Escape($EnvVarName))=" }
229
+ Set-Content -Path $EnvFile -Value $content -Encoding UTF8
230
+ Add-Content -Path $EnvFile -Value "$EnvVarName=$Token"
231
+ Write-Host "Updated $EnvVarName in $EnvFile"
232
+ } else {
233
+ Write-Host "Keeping existing value."
234
+ }
235
+ } else {
236
+ Add-Content -Path $EnvFile -Value "$EnvVarName=$Token"
237
+ Write-Host "Saved $EnvVarName to $EnvFile"
238
+ }
239
+
240
+ # 5. Backup and merge openclaw.json
241
+ if (-not (Test-Path $OpenclawJson)) { Set-Content -Path $OpenclawJson -Value "{}" -Encoding UTF8 }
242
+
243
+ $timestamp = Get-Date -Format "yyyyMMddHHmmss"
244
+ Copy-Item -Path $OpenclawJson -Destination "$OpenclawJson.bak-$timestamp" -Force
245
+ Write-Host "Backup: $OpenclawJson.bak-$timestamp"
246
+
247
+ $cfgRaw = Get-Content -Path $OpenclawJson -Raw
248
+ if ([string]::IsNullOrWhiteSpace($cfgRaw)) { $cfgRaw = "{}" }
249
+ $cfg = $cfgRaw | ConvertFrom-Json
250
+ $serverConfig = Get-Content -Path $ServerConfigFile -Raw | ConvertFrom-Json
251
+
252
+ $pathOverride = Get-PathOverride
253
+ if ($pathOverride -and $serverConfig.args -and $serverConfig.args.Count -gt 0) {
254
+ for ($i = 0; $i -lt $serverConfig.args.Count; $i++) {
255
+ if ($serverConfig.args[$i] -is [string] -and $serverConfig.args[$i].StartsWith("path/to/")) {
256
+ $serverConfig.args[$i] = $pathOverride
257
+ }
258
+ }
259
+ }
260
+
261
+ $plugins = Ensure-Property -Object $cfg -Name "plugins" -DefaultValue ([PSCustomObject]@{})
262
+ $allow = Ensure-Property -Object $plugins -Name "allow" -DefaultValue @()
263
+ if ($allow -notcontains "openclaw-mcp-bridge") { $plugins.allow = @($allow) + "openclaw-mcp-bridge" }
264
+ $entries = Ensure-Property -Object $plugins -Name "entries" -DefaultValue ([PSCustomObject]@{})
265
+
266
+ if (-not ($entries.PSObject.Properties.Name -contains "openclaw-mcp-bridge")) {
267
+ $entries | Add-Member -NotePropertyName "openclaw-mcp-bridge" -NotePropertyValue ([PSCustomObject]@{})
268
+ }
269
+ $mcpClient = $entries."openclaw-mcp-bridge"
270
+ if (-not ($mcpClient.PSObject.Properties.Name -contains "enabled")) {
271
+ $mcpClient | Add-Member -NotePropertyName "enabled" -NotePropertyValue $true
272
+ }
273
+ $mcpConfig = Ensure-Property -Object $mcpClient -Name "config" -DefaultValue ([PSCustomObject]@{})
274
+ if (-not ($mcpConfig.PSObject.Properties.Name -contains "toolPrefix")) { $mcpConfig | Add-Member -NotePropertyName "toolPrefix" -NotePropertyValue $true }
275
+ if (-not ($mcpConfig.PSObject.Properties.Name -contains "reconnectIntervalMs")) { $mcpConfig | Add-Member -NotePropertyName "reconnectIntervalMs" -NotePropertyValue 30000 }
276
+ if (-not ($mcpConfig.PSObject.Properties.Name -contains "connectionTimeoutMs")) { $mcpConfig | Add-Member -NotePropertyName "connectionTimeoutMs" -NotePropertyValue 10000 }
277
+ if (-not ($mcpConfig.PSObject.Properties.Name -contains "requestTimeoutMs")) { $mcpConfig | Add-Member -NotePropertyName "requestTimeoutMs" -NotePropertyValue 60000 }
278
+ $servers = Ensure-Property -Object $mcpConfig -Name "servers" -DefaultValue ([PSCustomObject]@{})
279
+
280
+ if ($servers.PSObject.Properties.Name -contains $ServerName) {
281
+ $servers.PSObject.Properties.Remove($ServerName)
282
+ }
283
+ $servers | Add-Member -NotePropertyName $ServerName -NotePropertyValue $serverConfig
284
+
285
+ $cfg | ConvertTo-Json -Depth 30 | Set-Content -Path $OpenclawJson -Encoding UTF8
286
+ Write-Host "Configuration merged for: $ServerName"
287
+
288
+ # 6. Gateway restart
289
+ Write-Host ""
290
+ $restart = Read-Host "Restart gateway now? [Y/n]"
291
+ if ([string]::IsNullOrWhiteSpace($restart) -or $restart -match "^[Yy]$") {
292
+ try {
293
+ openclaw gateway restart 2>$null
294
+ Write-Host "Gateway restarting... Check 'openclaw gateway status' in a moment."
295
+ } catch {
296
+ Write-Host "Could not restart automatically. Run: openclaw gateway restart"
297
+ }
298
+ } else {
299
+ Write-Host "Run manually: openclaw gateway restart"
300
+ }
@@ -0,0 +1,357 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ OPENCLAW_DIR="${HOME}/.openclaw"
6
+ OPENCLAW_JSON="${OPENCLAW_DIR}/openclaw.json"
7
+ ENV_FILE="${OPENCLAW_DIR}/.env"
8
+
9
+ usage() {
10
+ echo "Usage: $0 <server-name> [--dry-run] [--remove]"
11
+ echo ""
12
+ echo "Available servers:"
13
+ for server_dir in "$SCRIPT_DIR/servers"/*; do
14
+ [[ -d "$server_dir" ]] && echo " - $(basename "$server_dir")"
15
+ done
16
+ exit 1
17
+ }
18
+
19
+ [[ $# -eq 0 ]] && usage
20
+
21
+ SERVER_NAME="$1"
22
+ DRY_RUN=false
23
+ REMOVE=false
24
+ shift
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --dry-run) DRY_RUN=true ;;
28
+ --remove) REMOVE=true ;;
29
+ esac
30
+ shift
31
+ done
32
+
33
+ SERVER_DIR="$SCRIPT_DIR/servers/$SERVER_NAME"
34
+ if [[ ! -d "$SERVER_DIR" ]]; then
35
+ echo "Error: Server '$SERVER_NAME' not found."
36
+ usage
37
+ fi
38
+
39
+ SERVER_TITLE="$(tr '-' ' ' <<<"$SERVER_NAME" | awk '{for(i=1;i<=NF;i++){$i=toupper(substr($i,1,1))substr($i,2)};print}')"
40
+ SERVER_CONFIG_FILE="$SERVER_DIR/config.json"
41
+ ENV_VARS_FILE="$SERVER_DIR/env_vars"
42
+
43
+ require_cmd() {
44
+ if ! command -v "$1" >/dev/null 2>&1; then
45
+ echo "❌ Missing required command: $1"
46
+ exit 1
47
+ fi
48
+ }
49
+
50
+ get_token_url() {
51
+ case "$SERVER_NAME" in
52
+ apify) echo "https://console.apify.com/settings/integrations" ;;
53
+ github) echo "https://github.com/settings/tokens" ;;
54
+ google-maps) echo "https://console.cloud.google.com/apis/credentials" ;;
55
+ hetzner) echo "https://console.hetzner.cloud/" ;;
56
+ hostinger) echo "https://hpanel.hostinger.com/api" ;;
57
+ linear) echo "https://linear.app/settings/api" ;;
58
+ miro) echo "https://miro.com/app/settings/user-profile/apps" ;;
59
+ notion) echo "https://www.notion.so/my-integrations" ;;
60
+ stripe) echo "https://dashboard.stripe.com/apikeys" ;;
61
+ tavily) echo "https://app.tavily.com/home" ;;
62
+ todoist) echo "https://app.todoist.com/app/settings/integrations/developer" ;;
63
+ wise) echo "https://wise.com/settings/api-tokens" ;;
64
+ *) echo "" ;;
65
+ esac
66
+ }
67
+
68
+ check_prerequisites() {
69
+ case "$SERVER_NAME" in
70
+ github)
71
+ require_cmd docker ;;
72
+ linear|wise|hetzner)
73
+ require_cmd node; require_cmd npm ;;
74
+ *)
75
+ require_cmd node; require_cmd npx ;;
76
+ esac
77
+ }
78
+
79
+ install_dependencies() {
80
+ case "$SERVER_NAME" in
81
+ github)
82
+ echo "Pulling GitHub MCP server Docker image..."
83
+ docker pull ghcr.io/github/github-mcp-server ;;
84
+ linear)
85
+ echo "Installing @anthropic-pb/linear-mcp-server globally..."
86
+ npm install -g @anthropic-pb/linear-mcp-server ;;
87
+ wise)
88
+ local clone_dir="$SERVER_DIR/mcp-server"
89
+ if [ -d "$clone_dir/.git" ]; then
90
+ echo "Updating wise mcp-server..."; git -C "$clone_dir" pull --ff-only
91
+ else
92
+ echo "Cloning wise mcp-server..."; git clone https://github.com/Szotasz/wise-mcp.git "$clone_dir"
93
+ fi
94
+ echo "Building wise mcp-server..."
95
+ (cd "$clone_dir" && npm install && npm run build) ;;
96
+ hetzner)
97
+ local clone_dir="$SERVER_DIR/mcp-server"
98
+ if [ -d "$clone_dir/.git" ]; then
99
+ echo "Updating hetzner mcp-server..."; git -C "$clone_dir" pull --ff-only
100
+ else
101
+ echo "Cloning hetzner mcp-server..."; git clone https://github.com/dkruyt/mcp-hetzner.git "$clone_dir"
102
+ fi
103
+ echo "Building hetzner mcp-server..."
104
+ (cd "$clone_dir" && npm install && npm run build) ;;
105
+ esac
106
+ }
107
+
108
+ resolve_path_override() {
109
+ case "$SERVER_NAME" in
110
+ linear)
111
+ local npm_root; npm_root="$(npm root -g)"
112
+ if [ -f "$npm_root/@anthropic-pb/linear-mcp-server/dist/index.js" ]; then
113
+ echo "$npm_root/@anthropic-pb/linear-mcp-server/dist/index.js"
114
+ else
115
+ echo "$npm_root/@anthropic-pb/linear-mcp-server/build/index.js"
116
+ fi ;;
117
+ wise) echo "$SERVER_DIR/mcp-server/dist/cli.js" ;;
118
+ hetzner) echo "$SERVER_DIR/mcp-server/dist/index.js" ;;
119
+ *) echo "" ;;
120
+ esac
121
+ }
122
+
123
+ # ========================================
124
+ # REMOVE MODE
125
+ # ========================================
126
+ if [[ "$REMOVE" == "true" ]]; then
127
+ echo "========================================"
128
+ echo "Removing ${SERVER_TITLE} MCP Server"
129
+ echo "========================================"
130
+
131
+ if [[ ! -f "$OPENCLAW_JSON" ]]; then
132
+ echo "❌ Config not found: $OPENCLAW_JSON"
133
+ exit 1
134
+ fi
135
+
136
+ # Check if server exists in config
137
+ HAS_SERVER=$(python3 -c "
138
+ import json
139
+ with open('$OPENCLAW_JSON') as f:
140
+ cfg = json.load(f)
141
+ servers = cfg.get('plugins',{}).get('entries',{}).get('openclaw-mcp-bridge',{}).get('config',{}).get('servers',{})
142
+ print('yes' if '$SERVER_NAME' in servers else 'no')
143
+ " 2>/dev/null)
144
+
145
+ if [[ "$HAS_SERVER" != "yes" ]]; then
146
+ echo "ℹ️ Server '$SERVER_NAME' not found in config. Nothing to remove."
147
+ exit 0
148
+ fi
149
+
150
+ # Backup
151
+ BACKUP_FILE="${OPENCLAW_JSON}.bak-$(date +%Y%m%d%H%M%S)"
152
+ cp "$OPENCLAW_JSON" "$BACKUP_FILE"
153
+ echo "Backup: ${BACKUP_FILE}"
154
+
155
+ # Remove server entry from config (keep servers/<name>/ directory)
156
+ python3 -c "
157
+ import json
158
+ with open('$OPENCLAW_JSON') as f:
159
+ cfg = json.load(f)
160
+ servers = cfg['plugins']['entries']['openclaw-mcp-bridge']['config']['servers']
161
+ del servers['$SERVER_NAME']
162
+ with open('$OPENCLAW_JSON', 'w') as f:
163
+ json.dump(cfg, f, indent=2)
164
+ f.write('\n')
165
+ print('✅ Removed $SERVER_NAME from config')
166
+ print('ℹ️ Server recipe kept in servers/$SERVER_NAME/ (reinstall anytime)')
167
+ " 2>/dev/null
168
+
169
+ # Remove env var from .env if exists
170
+ if [[ -f "$ENV_VARS_FILE" ]] && [[ -s "$ENV_VARS_FILE" ]] && [[ -f "$ENV_FILE" ]]; then
171
+ ENV_VAR_NAME="$(head -n 1 "$ENV_VARS_FILE" | tr -d '[:space:]')"
172
+ if grep -q "^${ENV_VAR_NAME}=" "$ENV_FILE" 2>/dev/null; then
173
+ sed -i "/^${ENV_VAR_NAME}=/d" "$ENV_FILE"
174
+ echo "🔑 Removed ${ENV_VAR_NAME} from ${ENV_FILE}"
175
+ fi
176
+ fi
177
+
178
+ # Restart
179
+ echo ""
180
+ RESTART="y"
181
+ if [ -e /dev/tty ]; then
182
+ read -r -p "Restart gateway now? [Y/n]: " RESTART </dev/tty
183
+ fi
184
+ if [[ -z "$RESTART" || "$RESTART" =~ ^[Yy]$ ]]; then
185
+ systemctl --user restart openclaw-gateway 2>/dev/null || {
186
+ echo "⚠️ Auto-restart failed. Run: systemctl --user restart openclaw-gateway"
187
+ exit 0
188
+ }
189
+ sleep 3
190
+ if systemctl --user is-active --quiet openclaw-gateway 2>/dev/null; then
191
+ echo "✅ Gateway restarted. ${SERVER_TITLE} removed."
192
+ else
193
+ echo "❌ Gateway failed to start! Restoring backup..."
194
+ cp "$BACKUP_FILE" "$OPENCLAW_JSON"
195
+ systemctl --user restart openclaw-gateway 2>/dev/null
196
+ echo "Restored from backup."
197
+ fi
198
+ else
199
+ echo "⏭️ Run manually: systemctl --user restart openclaw-gateway"
200
+ fi
201
+ exit 0
202
+ fi
203
+
204
+ # ========================================
205
+ # INSTALL MODE
206
+ # ========================================
207
+ echo "========================================"
208
+ echo "Installing ${SERVER_TITLE} MCP Server"
209
+ echo "========================================"
210
+
211
+ if [[ "$DRY_RUN" == "true" ]]; then
212
+ echo "[DRY RUN] Server: $SERVER_NAME"
213
+ [[ -f "$ENV_VARS_FILE" ]] && echo "[DRY RUN] Env var: $(cat "$ENV_VARS_FILE")"
214
+ echo "[DRY RUN] Config:"; cat "$SERVER_CONFIG_FILE"
215
+ exit 0
216
+ fi
217
+
218
+ # 1. Check prerequisites
219
+ check_prerequisites
220
+
221
+ # 2. Install server-specific dependencies
222
+ install_dependencies
223
+
224
+ # 3. Get API token
225
+ if [[ -f "$ENV_VARS_FILE" ]] && [[ -s "$ENV_VARS_FILE" ]]; then
226
+ ENV_VAR_NAME="$(head -n 1 "$ENV_VARS_FILE" | tr -d '[:space:]')"
227
+
228
+ TOKEN_URL="$(get_token_url)"
229
+ [[ -n "$TOKEN_URL" ]] && echo "Get your API token here: ${TOKEN_URL}"
230
+
231
+ TOKEN=""
232
+ while [ -z "$TOKEN" ]; do
233
+ read -r -p "Enter your ${SERVER_TITLE} API token: " TOKEN </dev/tty
234
+ [[ -z "$TOKEN" ]] && echo "Token cannot be empty."
235
+ done
236
+
237
+ # Write to .env
238
+ mkdir -p "$OPENCLAW_DIR"
239
+ touch "$ENV_FILE"
240
+ chmod 600 "$ENV_FILE"
241
+
242
+ if grep -q "^${ENV_VAR_NAME}=" "$ENV_FILE" 2>/dev/null; then
243
+ echo "${ENV_VAR_NAME} already exists in ${ENV_FILE}."
244
+ read -r -p "Overwrite with new token? [y/N]: " OVERWRITE </dev/tty
245
+ if [[ "$OVERWRITE" =~ ^[Yy]$ ]]; then
246
+ sed -i "/^${ENV_VAR_NAME}=/d" "$ENV_FILE"
247
+ echo "${ENV_VAR_NAME}=${TOKEN}" >> "$ENV_FILE"
248
+ echo "✅ Updated ${ENV_VAR_NAME} in ${ENV_FILE}"
249
+ else
250
+ echo "Keeping existing value."
251
+ fi
252
+ else
253
+ echo "${ENV_VAR_NAME}=${TOKEN}" >> "$ENV_FILE"
254
+ echo "✅ Saved ${ENV_VAR_NAME} to ${ENV_FILE}"
255
+ fi
256
+ fi
257
+
258
+ # 4. Backup and merge openclaw.json
259
+ mkdir -p "$(dirname "$OPENCLAW_JSON")"
260
+ [[ ! -f "$OPENCLAW_JSON" ]] && echo "{}" > "$OPENCLAW_JSON"
261
+
262
+ BACKUP_FILE="${OPENCLAW_JSON}.bak-$(date +%Y%m%d%H%M%S)"
263
+ cp "$OPENCLAW_JSON" "$BACKUP_FILE"
264
+ echo "Backup: ${BACKUP_FILE}"
265
+
266
+ PATH_OVERRIDE="$(resolve_path_override)"
267
+
268
+ python3 - "$OPENCLAW_JSON" "$SERVER_CONFIG_FILE" "$SERVER_NAME" "$PATH_OVERRIDE" <<'PY'
269
+ import json, sys
270
+
271
+ openclaw_path, server_cfg_path, server_name, path_override = sys.argv[1:5]
272
+
273
+ with open(openclaw_path, "r", encoding="utf-8") as f:
274
+ raw = f.read().strip()
275
+ cfg = json.loads(raw) if raw else {}
276
+
277
+ with open(server_cfg_path, "r", encoding="utf-8") as f:
278
+ server_cfg = json.load(f)
279
+
280
+ if path_override:
281
+ args = server_cfg.get("args")
282
+ if isinstance(args, list):
283
+ for idx, value in enumerate(args):
284
+ if isinstance(value, str) and value.startswith("path/to/"):
285
+ args[idx] = path_override
286
+
287
+ plugins = cfg.setdefault("plugins", {})
288
+ allow = plugins.setdefault("allow", [])
289
+ if "openclaw-mcp-bridge" not in allow:
290
+ allow.append("openclaw-mcp-bridge")
291
+ entries = plugins.setdefault("entries", {})
292
+ mcp_client = entries.setdefault("openclaw-mcp-bridge", {})
293
+ mcp_client.setdefault("enabled", True)
294
+ mcp_cfg = mcp_client.setdefault("config", {})
295
+ mcp_cfg.setdefault("toolPrefix", True)
296
+ mcp_cfg.setdefault("reconnectIntervalMs", 30000)
297
+ mcp_cfg.setdefault("connectionTimeoutMs", 10000)
298
+ mcp_cfg.setdefault("requestTimeoutMs", 60000)
299
+ servers = mcp_cfg.setdefault("servers", {})
300
+ servers[server_name] = server_cfg
301
+
302
+ with open(openclaw_path, "w", encoding="utf-8") as f:
303
+ json.dump(cfg, f, indent=2)
304
+ f.write("\n")
305
+
306
+ print(f"✅ Configuration merged for: {server_name}")
307
+ PY
308
+
309
+ # 5. Gateway restart
310
+ echo ""
311
+ read -r -p "Restart gateway now? [Y/n]: " RESTART </dev/tty
312
+ if [[ -z "$RESTART" || "$RESTART" =~ ^[Yy]$ ]]; then
313
+ systemctl --user restart openclaw-gateway 2>/dev/null || {
314
+ echo "⚠️ Auto-restart failed. Run: systemctl --user restart openclaw-gateway"
315
+ exit 0
316
+ }
317
+ echo "Waiting for gateway startup..."
318
+ CONFIRMED=false
319
+ ROUTER_MODE=false
320
+ for i in 1 2 3 4 5 6; do
321
+ sleep 5
322
+ if ! systemctl --user is-active --quiet openclaw-gateway 2>/dev/null; then
323
+ echo "❌ Gateway failed to start!"
324
+ journalctl --user -u openclaw-gateway --since "1 min ago" --no-pager 2>/dev/null | grep -iE "error|fail|missing" | head -5
325
+ echo "Full logs: journalctl --user -u openclaw-gateway --since '1 min ago' --no-pager"
326
+ exit 1
327
+ fi
328
+ # Router mode: servers connect lazily, just check plugin loaded
329
+ if journalctl --user -u openclaw-gateway --since "1 min ago" --no-pager 2>/dev/null | grep -qi "Plugin activated with.*servers configured"; then
330
+ CONFIRMED=true
331
+ ROUTER_MODE=true
332
+ break
333
+ fi
334
+ # Direct mode: server connects at boot
335
+ if journalctl --user -u openclaw-gateway --since "1 min ago" --no-pager 2>/dev/null | grep -qi "Server ${SERVER_NAME} initialized"; then
336
+ CONFIRMED=true
337
+ break
338
+ fi
339
+ # Check if server explicitly failed
340
+ if journalctl --user -u openclaw-gateway --since "1 min ago" --no-pager 2>/dev/null | grep -qi "Startup failed: ${SERVER_NAME}"; then
341
+ echo "❌ ${SERVER_TITLE} MCP Server failed to start!"
342
+ journalctl --user -u openclaw-gateway --since "1 min ago" --no-pager 2>/dev/null | grep -i "$SERVER_NAME" | tail -5
343
+ exit 1
344
+ fi
345
+ done
346
+ if $CONFIRMED; then
347
+ if $ROUTER_MODE; then
348
+ echo "✅ ${SERVER_TITLE} configured! (Router mode — server connects on first use)"
349
+ else
350
+ echo "✅ ${SERVER_TITLE} MCP Server installed and running!"
351
+ fi
352
+ else
353
+ echo "⚠️ Gateway running but plugin not confirmed after 30s. Check: journalctl --user -u openclaw-gateway -f"
354
+ fi
355
+ else
356
+ echo "⏭️ Run manually: systemctl --user restart openclaw-gateway"
357
+ fi