@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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/mcp-bridge.js +9 -0
- package/bin/mcp-bridge.ts +335 -0
- package/package.json +42 -0
- package/scripts/install-server.ps1 +300 -0
- package/scripts/install-server.sh +357 -0
- package/servers/apify/README.md +40 -0
- package/servers/apify/config.json +13 -0
- package/servers/apify/env_vars +1 -0
- package/servers/apify/install.ps1 +3 -0
- package/servers/apify/install.sh +4 -0
- package/servers/candidates.md +13 -0
- package/servers/github/README.md +40 -0
- package/servers/github/config.json +21 -0
- package/servers/github/env_vars +1 -0
- package/servers/github/install.ps1 +3 -0
- package/servers/github/install.sh +4 -0
- package/servers/google-maps/README.md +40 -0
- package/servers/google-maps/config.json +17 -0
- package/servers/google-maps/env_vars +1 -0
- package/servers/google-maps/install.ps1 +3 -0
- package/servers/google-maps/install.sh +4 -0
- package/servers/hetzner/README.md +41 -0
- package/servers/hetzner/config.json +16 -0
- package/servers/hetzner/env_vars +1 -0
- package/servers/hetzner/install.ps1 +3 -0
- package/servers/hetzner/install.sh +4 -0
- package/servers/hostinger/README.md +40 -0
- package/servers/hostinger/config.json +17 -0
- package/servers/hostinger/env_vars +1 -0
- package/servers/hostinger/install.ps1 +3 -0
- package/servers/hostinger/install.sh +4 -0
- package/servers/index.json +125 -0
- package/servers/linear/README.md +40 -0
- package/servers/linear/config.json +16 -0
- package/servers/linear/env_vars +1 -0
- package/servers/linear/install.ps1 +3 -0
- package/servers/linear/install.sh +4 -0
- package/servers/miro/README.md +40 -0
- package/servers/miro/config.json +19 -0
- package/servers/miro/env_vars +1 -0
- package/servers/miro/install.ps1 +3 -0
- package/servers/miro/install.sh +4 -0
- package/servers/notion/README.md +42 -0
- package/servers/notion/config.json +17 -0
- package/servers/notion/env_vars +1 -0
- package/servers/notion/install.ps1 +3 -0
- package/servers/notion/install.sh +4 -0
- package/servers/stripe/README.md +40 -0
- package/servers/stripe/config.json +19 -0
- package/servers/stripe/env_vars +1 -0
- package/servers/stripe/install.ps1 +3 -0
- package/servers/stripe/install.sh +4 -0
- package/servers/tavily/README.md +40 -0
- package/servers/tavily/config.json +17 -0
- package/servers/tavily/env_vars +1 -0
- package/servers/tavily/install.ps1 +3 -0
- package/servers/tavily/install.sh +4 -0
- package/servers/todoist/README.md +40 -0
- package/servers/todoist/config.json +17 -0
- package/servers/todoist/env_vars +1 -0
- package/servers/todoist/install.ps1 +3 -0
- package/servers/todoist/install.sh +4 -0
- package/servers/wise/README.md +41 -0
- package/servers/wise/config.json +16 -0
- package/servers/wise/env_vars +1 -0
- package/servers/wise/install.ps1 +3 -0
- package/servers/wise/install.sh +4 -0
- package/src/config.ts +168 -0
- package/src/index.ts +44 -0
- package/src/mcp-router.ts +366 -0
- package/src/protocol.ts +69 -0
- package/src/schema-convert.ts +178 -0
- package/src/standalone-server.ts +385 -0
- package/src/tool-naming.ts +51 -0
- package/src/transport-base.ts +199 -0
- package/src/transport-sse.ts +230 -0
- package/src/transport-stdio.ts +312 -0
- package/src/transport-streamable-http.ts +188 -0
- package/src/types.ts +88 -0
- package/src/update-checker.ts +155 -0
- package/tests/collision.test.ts +60 -0
- package/tests/env-resolve.test.ts +68 -0
- package/tests/mcp-router.test.ts +301 -0
- package/tests/schema-convert.test.ts +70 -0
- package/tests/transport-base.test.ts +214 -0
- 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
|