openclaw-mcp 1.4.0 → 1.5.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/README.md +41 -4
- package/dist/index.js +59 -28
- package/docs/configuration.md +20 -7
- package/docs/deployment.md +14 -2
- package/docs/development.md +1 -1
- package/docs/installation.md +8 -5
- package/docs/logging.md +3 -3
- package/docs/threat-model.md +3 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.freema/openclaw-mcp -->
|
|
2
|
+
|
|
1
3
|
# OpenClaw MCP Server
|
|
2
4
|
|
|
3
5
|
[](https://www.npmjs.com/package/openclaw-mcp)
|
|
@@ -52,6 +54,7 @@ services:
|
|
|
52
54
|
- MCP_CLIENT_ID=openclaw
|
|
53
55
|
- MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET}
|
|
54
56
|
- MCP_ISSUER_URL=${MCP_ISSUER_URL:-}
|
|
57
|
+
- TRUST_PROXY=1
|
|
55
58
|
- CORS_ORIGINS=https://claude.ai
|
|
56
59
|
extra_hosts:
|
|
57
60
|
- "host.docker.internal:host-gateway"
|
|
@@ -68,7 +71,9 @@ export OPENCLAW_GATEWAY_TOKEN=your-gateway-token
|
|
|
68
71
|
docker compose up -d
|
|
69
72
|
```
|
|
70
73
|
|
|
71
|
-
Then in Claude.ai add a custom MCP connector pointing to your
|
|
74
|
+
Then in Claude.ai add a custom MCP connector pointing to `https://your-domain.com/mcp` with `MCP_CLIENT_ID=openclaw` and your `MCP_CLIENT_SECRET`.
|
|
75
|
+
|
|
76
|
+
> **Important:** The connector URL **must end with `/mcp`** — that's the Streamable HTTP endpoint. A bare domain (`https://your-domain.com`) hits the server root and returns 404 after OAuth completes.
|
|
72
77
|
|
|
73
78
|
> **Tip:** Pin a specific version instead of `latest` for production: `ghcr.io/freema/openclaw-mcp:1.1.0`
|
|
74
79
|
|
|
@@ -103,10 +108,12 @@ Add to your Claude Desktop config:
|
|
|
103
108
|
AUTH_ENABLED=true MCP_CLIENT_ID=openclaw MCP_CLIENT_SECRET=your-secret \
|
|
104
109
|
MCP_ISSUER_URL=https://mcp.your-domain.com \
|
|
105
110
|
CORS_ORIGINS=https://claude.ai OPENCLAW_GATEWAY_TOKEN=your-gateway-token \
|
|
106
|
-
npx openclaw-mcp --transport
|
|
111
|
+
npx openclaw-mcp --transport http --port 3000
|
|
107
112
|
```
|
|
108
113
|
|
|
109
|
-
> **Important:** When running behind a reverse proxy (Caddy, nginx, etc.)
|
|
114
|
+
> **Important:** When running behind a reverse proxy (Caddy, nginx, Traefik, Cloudflare Tunnel, etc.) you **must** set:
|
|
115
|
+
> - `MCP_ISSUER_URL` (or `--issuer-url`) to your public HTTPS URL — otherwise OAuth metadata advertises `http://localhost:3000` and clients fail to authenticate.
|
|
116
|
+
> - `TRUST_PROXY=1` (or `--trust-proxy 1`) — otherwise `express-rate-limit` rejects the proxy's `X-Forwarded-For` header and `/token` crashes with `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR`.
|
|
110
117
|
|
|
111
118
|
See [Installation Guide](docs/installation.md) for details.
|
|
112
119
|
|
|
@@ -248,7 +255,7 @@ export MCP_CLIENT_SECRET=$(openssl rand -hex 32)
|
|
|
248
255
|
|
|
249
256
|
# Run with auth enabled
|
|
250
257
|
AUTH_ENABLED=true MCP_CLIENT_ID=openclaw MCP_CLIENT_SECRET=$MCP_CLIENT_SECRET \
|
|
251
|
-
openclaw-mcp --transport
|
|
258
|
+
openclaw-mcp --transport http
|
|
252
259
|
```
|
|
253
260
|
|
|
254
261
|
Configure CORS to restrict access:
|
|
@@ -259,6 +266,36 @@ CORS_ORIGINS=https://claude.ai,https://your-app.com
|
|
|
259
266
|
|
|
260
267
|
See [Configuration](docs/configuration.md) for all security options.
|
|
261
268
|
|
|
269
|
+
## Migrating from SSE to HTTP transport
|
|
270
|
+
|
|
271
|
+
Starting with v1.5.0, the primary transport is **Streamable HTTP** (`--transport http`). The legacy SSE transport (`--transport sse`) is deprecated but still works for backward compatibility.
|
|
272
|
+
|
|
273
|
+
### What changed
|
|
274
|
+
|
|
275
|
+
| Before | After |
|
|
276
|
+
|--------|-------|
|
|
277
|
+
| `--transport sse` | `--transport http` (recommended) |
|
|
278
|
+
| Primary endpoint: `GET /sse` | Primary endpoint: `POST/GET/DELETE /mcp` |
|
|
279
|
+
| Health: `"transport": "sse"` | Health: `"transport": "streamable-http"` |
|
|
280
|
+
|
|
281
|
+
### Migration steps
|
|
282
|
+
|
|
283
|
+
1. **CLI / Docker**: Replace `--transport sse` with `--transport http`
|
|
284
|
+
```bash
|
|
285
|
+
# Before
|
|
286
|
+
openclaw-mcp --transport sse --port 3000
|
|
287
|
+
# After
|
|
288
|
+
openclaw-mcp --transport http --port 3000
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
2. **Claude.ai connector URL**: No change needed — Claude.ai already uses `/mcp` (Streamable HTTP)
|
|
292
|
+
|
|
293
|
+
3. **Legacy clients**: The `/sse` and `/messages` endpoints still work. A deprecation warning is logged on each SSE connection.
|
|
294
|
+
|
|
295
|
+
4. **Dockerfile ENTRYPOINT**: Updated automatically if using the official Docker image
|
|
296
|
+
|
|
297
|
+
> **Note:** `--transport sse` will continue to work as a deprecated alias. Both transports are served simultaneously regardless of which flag you use.
|
|
298
|
+
|
|
262
299
|
## Requirements
|
|
263
300
|
|
|
264
301
|
- Node.js ≥ 20
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
|
|
6
6
|
// src/config/constants.ts
|
|
7
7
|
var SERVER_NAME = "openclaw-mcp";
|
|
8
|
-
var SERVER_VERSION = "1.
|
|
8
|
+
var SERVER_VERSION = "1.5.0";
|
|
9
9
|
var DEFAULT_OPENCLAW_URL = "http://127.0.0.1:18789";
|
|
10
10
|
var DEFAULT_MODEL = "openclaw";
|
|
11
11
|
var SERVER_ICON_SVG_BASE64 = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgZmlsbD0ibm9uZSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzFhMWEyZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzE2MjEzZSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjbGF3IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZmYzMzMzIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjY2MwMDAwIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIyNCIgZmlsbD0idXJsKCNiZykiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NCA2NCkiIHN0cm9rZT0idXJsKCNjbGF3KSIgc3Ryb2tlLXdpZHRoPSI3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiPjxwYXRoIGQ9Ik0tMjggLTM4YzAgMCAtMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0tMTIgLTQwYzAgMCAtNiAyMiA0IDM0Ii8+PHBhdGggZD0iTTI4IC0zOGMwIDAgMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0xMiAtNDBjMCAwIDYgMjIgLTQgMzQiLz48Y2lyY2xlIGN4PSIwIiBjeT0iMTAiIHI9IjIwIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNLTEwIDR2LTQiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Ik0xMCA0di00IiBzdHJva2Utd2lkdGg9IjQiLz48cGF0aCBkPSJNLTggMjBjNCA2IDEyIDYgMTYgMCIgc3Ryb2tlLXdpZHRoPSIzIi8+PC9nPjwvc3ZnPg==";
|
|
@@ -74,17 +74,17 @@ function parseArguments(version) {
|
|
|
74
74
|
}).option("transport", {
|
|
75
75
|
alias: "t",
|
|
76
76
|
type: "string",
|
|
77
|
-
choices: ["stdio", "sse"],
|
|
78
|
-
description:
|
|
77
|
+
choices: ["stdio", "http", "sse"],
|
|
78
|
+
description: 'Transport mode (stdio for local, http for remote; "sse" is a deprecated alias for "http")',
|
|
79
79
|
default: "stdio"
|
|
80
80
|
}).option("port", {
|
|
81
81
|
alias: "p",
|
|
82
82
|
type: "number",
|
|
83
|
-
description: "Port for
|
|
83
|
+
description: "Port for HTTP server",
|
|
84
84
|
default: parseInt(process.env.PORT || "3000", 10)
|
|
85
85
|
}).option("host", {
|
|
86
86
|
type: "string",
|
|
87
|
-
description: "Host for
|
|
87
|
+
description: "Host for HTTP server",
|
|
88
88
|
default: process.env.HOST || "0.0.0.0"
|
|
89
89
|
}).option("timeout", {
|
|
90
90
|
type: "number",
|
|
@@ -96,7 +96,7 @@ function parseArguments(version) {
|
|
|
96
96
|
default: process.env.DEBUG === "true" || process.env.NODE_ENV === "development"
|
|
97
97
|
}).option("auth", {
|
|
98
98
|
type: "boolean",
|
|
99
|
-
description: "Enable OAuth authentication (
|
|
99
|
+
description: "Enable OAuth authentication (HTTP mode)",
|
|
100
100
|
default: process.env.AUTH_ENABLED === "true" || process.env.OAUTH_ENABLED === "true"
|
|
101
101
|
}).option("client-id", {
|
|
102
102
|
type: "string",
|
|
@@ -118,6 +118,10 @@ function parseArguments(version) {
|
|
|
118
118
|
type: "boolean",
|
|
119
119
|
description: "Allow OAuth Dynamic Client Registration (Cursor/Windsurf compatibility, dev-only)",
|
|
120
120
|
default: process.env.MCP_DANGEROUSLY_ALLOW_DCR === "true"
|
|
121
|
+
}).option("trust-proxy", {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: 'Express trust proxy setting when behind a reverse proxy (e.g. "1", "true", or a CIDR)',
|
|
124
|
+
default: process.env.TRUST_PROXY || void 0
|
|
121
125
|
}).help().parseSync();
|
|
122
126
|
let instances;
|
|
123
127
|
const instancesEnv = process.env.OPENCLAW_INSTANCES;
|
|
@@ -173,6 +177,7 @@ function parseArguments(version) {
|
|
|
173
177
|
issuerUrl: argv["issuer-url"],
|
|
174
178
|
redirectUris: argv["redirect-uris"] ? argv["redirect-uris"].split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
175
179
|
allowDcr: argv["allow-dcr"],
|
|
180
|
+
trustProxy: argv["trust-proxy"],
|
|
176
181
|
instances
|
|
177
182
|
};
|
|
178
183
|
}
|
|
@@ -1152,7 +1157,7 @@ function registerTools(server, deps2) {
|
|
|
1152
1157
|
});
|
|
1153
1158
|
}
|
|
1154
1159
|
|
|
1155
|
-
// src/server/
|
|
1160
|
+
// src/server/http.ts
|
|
1156
1161
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1157
1162
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
1158
1163
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -1369,7 +1374,23 @@ var OpenClawAuthProvider = class {
|
|
|
1369
1374
|
}
|
|
1370
1375
|
};
|
|
1371
1376
|
|
|
1372
|
-
// src/server/
|
|
1377
|
+
// src/server/http.ts
|
|
1378
|
+
function parseTrustProxy(value) {
|
|
1379
|
+
if (value === void 0 || value === "") {
|
|
1380
|
+
return void 0;
|
|
1381
|
+
}
|
|
1382
|
+
const trimmed = value.trim();
|
|
1383
|
+
if (trimmed === "") {
|
|
1384
|
+
return void 0;
|
|
1385
|
+
}
|
|
1386
|
+
const lower = trimmed.toLowerCase();
|
|
1387
|
+
if (lower === "true") return true;
|
|
1388
|
+
if (lower === "false") return false;
|
|
1389
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1390
|
+
return parseInt(trimmed, 10);
|
|
1391
|
+
}
|
|
1392
|
+
return trimmed;
|
|
1393
|
+
}
|
|
1373
1394
|
function loadCorsConfig() {
|
|
1374
1395
|
const corsOrigins = process.env.CORS_ORIGINS;
|
|
1375
1396
|
if (!corsOrigins || corsOrigins === "*") {
|
|
@@ -1399,12 +1420,16 @@ function isOriginAllowed(origin, allowedOrigins) {
|
|
|
1399
1420
|
return origin === allowed || origin === `https://${allowed}` || origin === `http://${allowed}`;
|
|
1400
1421
|
});
|
|
1401
1422
|
}
|
|
1402
|
-
async function
|
|
1423
|
+
async function createHttpServer(config, deps2) {
|
|
1403
1424
|
const authEnabled = !!config.authConfig?.clientId;
|
|
1404
1425
|
const corsConfig = loadCorsConfig();
|
|
1405
|
-
const
|
|
1426
|
+
const legacySseSessions = /* @__PURE__ */ new Map();
|
|
1406
1427
|
const streamableSessions = /* @__PURE__ */ new Map();
|
|
1407
1428
|
const app = createMcpExpressApp({ host: config.host });
|
|
1429
|
+
if (config.trustProxy !== void 0) {
|
|
1430
|
+
app.set("trust proxy", config.trustProxy);
|
|
1431
|
+
log(`Trust proxy: ${JSON.stringify(config.trustProxy)}`);
|
|
1432
|
+
}
|
|
1408
1433
|
app.use((req, res, next) => {
|
|
1409
1434
|
if (!corsConfig.enabled) {
|
|
1410
1435
|
next();
|
|
@@ -1450,7 +1475,8 @@ async function createSSEServer(config, deps2) {
|
|
|
1450
1475
|
app.get("/health", (_req, res) => {
|
|
1451
1476
|
res.json({
|
|
1452
1477
|
status: "ok",
|
|
1453
|
-
transport: "
|
|
1478
|
+
transport: "streamable-http",
|
|
1479
|
+
legacySseSupported: true,
|
|
1454
1480
|
auth: authEnabled
|
|
1455
1481
|
});
|
|
1456
1482
|
});
|
|
@@ -1463,19 +1489,20 @@ async function createSSEServer(config, deps2) {
|
|
|
1463
1489
|
app.get(
|
|
1464
1490
|
"/sse",
|
|
1465
1491
|
...withAuth(async (req, res) => {
|
|
1492
|
+
log("Legacy SSE transport is deprecated; prefer Streamable HTTP at /mcp");
|
|
1466
1493
|
const transport = new SSEServerTransport("/messages", res);
|
|
1467
1494
|
const server = createMcpServer(deps2);
|
|
1468
1495
|
const sessionId = transport.sessionId;
|
|
1469
|
-
|
|
1496
|
+
legacySseSessions.set(sessionId, { transport, server });
|
|
1470
1497
|
log(`SSE session connected: ${sessionId}`);
|
|
1471
1498
|
transport.onclose = () => {
|
|
1472
|
-
|
|
1499
|
+
legacySseSessions.delete(sessionId);
|
|
1473
1500
|
log(`SSE session disconnected: ${sessionId}`);
|
|
1474
1501
|
};
|
|
1475
1502
|
try {
|
|
1476
1503
|
await server.connect(transport);
|
|
1477
1504
|
} catch (error) {
|
|
1478
|
-
|
|
1505
|
+
legacySseSessions.delete(sessionId);
|
|
1479
1506
|
logError(`Failed to connect SSE session ${sessionId}`, error);
|
|
1480
1507
|
}
|
|
1481
1508
|
})
|
|
@@ -1484,7 +1511,7 @@ async function createSSEServer(config, deps2) {
|
|
|
1484
1511
|
"/messages",
|
|
1485
1512
|
...withAuth(async (req, res) => {
|
|
1486
1513
|
const sessionId = req.query.sessionId;
|
|
1487
|
-
const session =
|
|
1514
|
+
const session = legacySseSessions.get(sessionId);
|
|
1488
1515
|
if (!session) {
|
|
1489
1516
|
res.status(404).json({ error: "Session not found" });
|
|
1490
1517
|
return;
|
|
@@ -1554,7 +1581,7 @@ async function createSSEServer(config, deps2) {
|
|
|
1554
1581
|
app.post("/mcp", ...withAuth(handleStreamableRequest));
|
|
1555
1582
|
app.delete("/mcp", ...withAuth(handleStreamableRequest));
|
|
1556
1583
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
1557
|
-
log(`
|
|
1584
|
+
log(`HTTP server listening on ${config.host}:${config.port}`);
|
|
1558
1585
|
log(`Auth enabled: ${authEnabled}`);
|
|
1559
1586
|
log(`CORS origins: ${corsConfig.enabled ? corsConfig.origins.join(", ") : "disabled"}`);
|
|
1560
1587
|
if (authEnabled) {
|
|
@@ -1569,20 +1596,20 @@ async function createSSEServer(config, deps2) {
|
|
|
1569
1596
|
}
|
|
1570
1597
|
log("MCP Endpoints:");
|
|
1571
1598
|
log(" GET /health - Health check (no auth)");
|
|
1572
|
-
log("
|
|
1573
|
-
log("
|
|
1574
|
-
log("
|
|
1599
|
+
log(" ALL /mcp - Streamable HTTP (primary)");
|
|
1600
|
+
log(" GET /sse - Legacy SSE stream (deprecated)");
|
|
1601
|
+
log(" POST /messages - Legacy SSE messages (deprecated)");
|
|
1575
1602
|
});
|
|
1576
1603
|
const shutdown = async () => {
|
|
1577
|
-
log("Shutting down
|
|
1578
|
-
for (const [id, session] of
|
|
1604
|
+
log("Shutting down HTTP server...");
|
|
1605
|
+
for (const [id, session] of legacySseSessions) {
|
|
1579
1606
|
try {
|
|
1580
1607
|
await session.server.close();
|
|
1581
1608
|
} catch (error) {
|
|
1582
1609
|
logError(`Error closing SSE session ${id}`, error);
|
|
1583
1610
|
}
|
|
1584
1611
|
}
|
|
1585
|
-
|
|
1612
|
+
legacySseSessions.clear();
|
|
1586
1613
|
for (const [id, session] of streamableSessions) {
|
|
1587
1614
|
try {
|
|
1588
1615
|
await session.server.close();
|
|
@@ -1592,7 +1619,7 @@ async function createSSEServer(config, deps2) {
|
|
|
1592
1619
|
}
|
|
1593
1620
|
streamableSessions.clear();
|
|
1594
1621
|
httpServer.close(() => {
|
|
1595
|
-
log("
|
|
1622
|
+
log("HTTP server stopped");
|
|
1596
1623
|
process.exit(0);
|
|
1597
1624
|
});
|
|
1598
1625
|
setTimeout(() => {
|
|
@@ -1631,11 +1658,15 @@ async function main() {
|
|
|
1631
1658
|
const defaultLabel = instance.isDefault ? " (default)" : "";
|
|
1632
1659
|
log(`Instance "${instance.name}": ${instance.url}${defaultLabel}`);
|
|
1633
1660
|
}
|
|
1634
|
-
if (args.transport === "sse") {
|
|
1635
|
-
|
|
1661
|
+
if (args.transport === "sse" || args.transport === "http") {
|
|
1662
|
+
if (args.transport === "sse") {
|
|
1663
|
+
log("WARNING: --transport sse is deprecated; use --transport http instead");
|
|
1664
|
+
}
|
|
1665
|
+
const httpConfig = {
|
|
1636
1666
|
port: args.port,
|
|
1637
1667
|
host: args.host,
|
|
1638
|
-
issuerUrl: args.issuerUrl
|
|
1668
|
+
issuerUrl: args.issuerUrl,
|
|
1669
|
+
trustProxy: parseTrustProxy(args.trustProxy)
|
|
1639
1670
|
};
|
|
1640
1671
|
if (args.authEnabled && args.clientId) {
|
|
1641
1672
|
const clientIdRegex = /^[a-zA-Z0-9][a-zA-Z0-9_-]{2,63}$/;
|
|
@@ -1651,7 +1682,7 @@ async function main() {
|
|
|
1651
1682
|
);
|
|
1652
1683
|
process.exit(1);
|
|
1653
1684
|
}
|
|
1654
|
-
|
|
1685
|
+
httpConfig.authConfig = {
|
|
1655
1686
|
clientId: args.clientId,
|
|
1656
1687
|
clientSecret: args.clientSecret,
|
|
1657
1688
|
redirectUris: args.redirectUris,
|
|
@@ -1685,7 +1716,7 @@ async function main() {
|
|
|
1685
1716
|
logError("AUTH_ENABLED=true but MCP_CLIENT_ID is not set. Refusing to start without auth.");
|
|
1686
1717
|
process.exit(1);
|
|
1687
1718
|
}
|
|
1688
|
-
await
|
|
1719
|
+
await createHttpServer(httpConfig, deps);
|
|
1689
1720
|
} else {
|
|
1690
1721
|
const server = createMcpServer(deps);
|
|
1691
1722
|
const transport = new StdioServerTransport();
|
package/docs/configuration.md
CHANGED
|
@@ -100,13 +100,26 @@ services:
|
|
|
100
100
|
|
|
101
101
|
**Backward compatibility:** When `OPENCLAW_INSTANCES` is not set, the server creates a single `"default"` instance from `OPENCLAW_URL` + `OPENCLAW_GATEWAY_TOKEN`. Existing deployments work without any configuration change — zero migration required.
|
|
102
102
|
|
|
103
|
-
### Server Settings (
|
|
104
|
-
|
|
105
|
-
| Variable
|
|
106
|
-
|
|
|
107
|
-
| `PORT`
|
|
108
|
-
| `HOST`
|
|
109
|
-
| `DEBUG`
|
|
103
|
+
### Server Settings (HTTP transport)
|
|
104
|
+
|
|
105
|
+
| Variable | Description | Default |
|
|
106
|
+
| ------------- | ----------------------------------------------------------------- | ------------------------ |
|
|
107
|
+
| `PORT` | HTTP server port | `3000` |
|
|
108
|
+
| `HOST` | HTTP server host | `0.0.0.0` |
|
|
109
|
+
| `DEBUG` | Enable debug logging | `false` |
|
|
110
|
+
| `TRUST_PROXY` | Express `trust proxy` setting (required behind a reverse proxy) | (unset — trust nothing) |
|
|
111
|
+
|
|
112
|
+
**`TRUST_PROXY` values:**
|
|
113
|
+
|
|
114
|
+
| Value | Meaning |
|
|
115
|
+
| -------------- | ------------------------------------------------------------------------------ |
|
|
116
|
+
| (unset) | Express default — do not trust any proxy |
|
|
117
|
+
| `1` | Trust one hop — typical when there is a single reverse proxy in front |
|
|
118
|
+
| `true` | Trust every proxy — only safe on a fully private network |
|
|
119
|
+
| `loopback` | Trust `127.0.0.1` and `::1` |
|
|
120
|
+
| `10.0.0.0/8` | Trust a specific IP or CIDR range |
|
|
121
|
+
|
|
122
|
+
When `TRUST_PROXY` is unset and a reverse proxy injects an `X-Forwarded-For` header, the MCP SDK's `/token` handler crashes with `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` and OAuth fails. Set this whenever the server is behind Caddy, nginx, Traefik, Cloudflare Tunnel, ngrok, or any other proxy.
|
|
110
123
|
|
|
111
124
|
### CORS Configuration
|
|
112
125
|
|
package/docs/deployment.md
CHANGED
|
@@ -50,7 +50,7 @@ OPENCLAW_GATEWAY_TOKEN=your-gateway-token
|
|
|
50
50
|
MCP_CLIENT_ID=openclaw
|
|
51
51
|
MCP_CLIENT_SECRET=your-client-secret
|
|
52
52
|
|
|
53
|
-
# Enable OAuth (required for production
|
|
53
|
+
# Enable OAuth (required for production HTTP transport)
|
|
54
54
|
AUTH_ENABLED=true
|
|
55
55
|
|
|
56
56
|
# Public URL (required when behind a reverse proxy)
|
|
@@ -78,6 +78,7 @@ docker compose up -d
|
|
|
78
78
|
- [ ] `MCP_CLIENT_ID` is valid (3–64 chars, alphanumeric/dashes/underscores)
|
|
79
79
|
- [ ] `MCP_CLIENT_SECRET` generated securely (`openssl rand -hex 32`, min 32 chars)
|
|
80
80
|
- [ ] `MCP_ISSUER_URL` set to public HTTPS URL (when behind reverse proxy)
|
|
81
|
+
- [ ] `TRUST_PROXY` set to the right hop count / CIDR (when behind reverse proxy)
|
|
81
82
|
- [ ] `MCP_REDIRECT_URIS` restricted to known callback URLs
|
|
82
83
|
- [ ] CORS restricted to known origins (`CORS_ORIGINS=https://claude.ai`)
|
|
83
84
|
- [ ] `OPENCLAW_GATEWAY_TOKEN` set for gateway authentication
|
|
@@ -88,7 +89,9 @@ docker compose up -d
|
|
|
88
89
|
|
|
89
90
|
The MCP bridge must be served over HTTPS for production use. Use a reverse proxy that handles TLS termination.
|
|
90
91
|
|
|
91
|
-
> **Important:**
|
|
92
|
+
> **Important:** When running behind a reverse proxy you **must** set both:
|
|
93
|
+
> - `MCP_ISSUER_URL` to your public HTTPS URL — otherwise OAuth metadata endpoints advertise `http://localhost:3000` and MCP clients (including Claude.ai) fail to authenticate with `Protected resource http://localhost:3000/mcp does not match expected https://your-domain.com/mcp`.
|
|
94
|
+
> - `TRUST_PROXY=1` so Express trusts the proxy's `X-Forwarded-For` header — otherwise `express-rate-limit` (used by the MCP SDK auth handlers) crashes `/token` with `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR`.
|
|
92
95
|
|
|
93
96
|
### Caddy (recommended)
|
|
94
97
|
|
|
@@ -122,6 +125,7 @@ services:
|
|
|
122
125
|
- "3000"
|
|
123
126
|
environment:
|
|
124
127
|
- MCP_ISSUER_URL=https://mcp.your-domain.com
|
|
128
|
+
- TRUST_PROXY=1
|
|
125
129
|
# ... other env vars
|
|
126
130
|
|
|
127
131
|
volumes:
|
|
@@ -197,6 +201,14 @@ The OpenClaw gateway's HTTP chat completions endpoint is disabled by default. En
|
|
|
197
201
|
|
|
198
202
|
You're running behind a reverse proxy but haven't set `MCP_ISSUER_URL`. The OAuth metadata endpoints are advertising `http://localhost:3000` instead of your public HTTPS URL. Set `MCP_ISSUER_URL` to your public URL (e.g., `https://mcp.your-domain.com`) or pass `--issuer-url` on the CLI.
|
|
199
203
|
|
|
204
|
+
### `POST /` or `GET /` returns 404 after OAuth succeeds
|
|
205
|
+
|
|
206
|
+
Your Claude.ai connector URL is missing the `/mcp` path. The MCP Streamable HTTP transport is mounted at `/mcp`, not at the server root (which is intentional — root is reserved for `/health`, `/.well-known/*`, OAuth endpoints, and legacy SSE endpoints). Update the connector URL in Claude.ai to end with `/mcp`, e.g. `https://mcp.your-domain.com/mcp`.
|
|
207
|
+
|
|
208
|
+
### `ValidationError: ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` on `/token`
|
|
209
|
+
|
|
210
|
+
The server is behind a reverse proxy that sets `X-Forwarded-For`, but Express's `trust proxy` is left at its default (`false`). The MCP SDK's OAuth handlers use `express-rate-limit`, which refuses to read the forwarded header in that configuration and crashes the request. Set `TRUST_PROXY=1` (single proxy in front) or `--trust-proxy 1`. Use a higher hop count, a CIDR/IP, or a keyword (`loopback`, `linklocal`, `uniquelocal`) for more complex topologies — see [Server Settings](configuration.md#server-settings-http-transport).
|
|
211
|
+
|
|
200
212
|
### `fetch failed` / MCP bridge can't reach gateway
|
|
201
213
|
|
|
202
214
|
When both services run in Docker, the MCP bridge must connect via the Docker network hostname (e.g., `http://openclaw-gateway:18789`), not `localhost`. Make sure both containers are on the same Docker network.
|
package/docs/development.md
CHANGED
|
@@ -48,7 +48,7 @@ src/
|
|
|
48
48
|
├── mcp/
|
|
49
49
|
│ └── tools/ # MCP tool implementations
|
|
50
50
|
├── openclaw/ # OpenClaw API client
|
|
51
|
-
├── server/ #
|
|
51
|
+
├── server/ # HTTP server (for remote access)
|
|
52
52
|
├── utils/ # Logging, errors, helpers
|
|
53
53
|
├── cli.ts # CLI argument parsing
|
|
54
54
|
└── index.ts # Main entry point
|
package/docs/installation.md
CHANGED
|
@@ -38,7 +38,7 @@ For local use with Claude Desktop, use stdio transport (default):
|
|
|
38
38
|
|
|
39
39
|
## Claude.ai (Remote Access)
|
|
40
40
|
|
|
41
|
-
For remote access via Claude.ai, deploy with
|
|
41
|
+
For remote access via Claude.ai, deploy with HTTP transport and OAuth 2.1 authentication.
|
|
42
42
|
|
|
43
43
|
### 1. Generate credentials
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@ MCP_CLIENT_ID=openclaw \
|
|
|
57
57
|
MCP_CLIENT_SECRET=your-secret \
|
|
58
58
|
CORS_ORIGINS=https://claude.ai \
|
|
59
59
|
OPENCLAW_GATEWAY_TOKEN=your-gateway-token \
|
|
60
|
-
openclaw-mcp --transport
|
|
60
|
+
openclaw-mcp --transport http --port 3000
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
### 3. Add to Claude.ai
|
|
@@ -92,17 +92,20 @@ Options:
|
|
|
92
92
|
--openclaw-url, -u OpenClaw gateway URL [default: "http://127.0.0.1:18789"]
|
|
93
93
|
--gateway-token Bearer token for gateway [default: none]
|
|
94
94
|
--model, -m Model name for chat [default: "openclaw"]
|
|
95
|
-
--transport, -t Transport mode [choices: "stdio", "sse"] [default: "stdio"]
|
|
96
|
-
--port, -p Port for
|
|
97
|
-
--host Host for
|
|
95
|
+
--transport, -t Transport mode [choices: "stdio", "http", "sse"] [default: "stdio"]
|
|
96
|
+
--port, -p Port for HTTP server [default: 3000]
|
|
97
|
+
--host Host for HTTP server [default: "0.0.0.0"]
|
|
98
98
|
--debug Enable debug logging [default: false]
|
|
99
99
|
--auth Enable OAuth [default: false]
|
|
100
100
|
--client-id MCP OAuth client ID [env: MCP_CLIENT_ID]
|
|
101
101
|
--client-secret MCP OAuth client secret [env: MCP_CLIENT_SECRET]
|
|
102
102
|
--issuer-url OAuth issuer URL [env: MCP_ISSUER_URL]
|
|
103
103
|
--redirect-uris Allowed redirect URIs [env: MCP_REDIRECT_URIS]
|
|
104
|
+
--trust-proxy Express trust proxy [env: TRUST_PROXY]
|
|
104
105
|
--version Show version number
|
|
105
106
|
--help Show help
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
> **Note:** `--issuer-url` is required when running behind a reverse proxy (Caddy, nginx, etc.) so that OAuth metadata endpoints return the correct public HTTPS URL instead of `http://localhost:3000`.
|
|
110
|
+
|
|
111
|
+
> **Note:** `--trust-proxy` (or `TRUST_PROXY`) is also required when running behind a reverse proxy. Use `1` for a single proxy, `true` on a fully private network, or a CIDR/keyword (`loopback`, `linklocal`, `uniquelocal`) for more specific setups. Without it, `/token` crashes with `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR`.
|
package/docs/logging.md
CHANGED
|
@@ -18,10 +18,10 @@ The MCP server logs operational events to **stderr** using the `[openclaw-mcp]`
|
|
|
18
18
|
|
|
19
19
|
- Server name and version
|
|
20
20
|
- OpenClaw gateway URL (host only, no tokens)
|
|
21
|
-
- Transport type (stdio or
|
|
21
|
+
- Transport type (stdio or HTTP)
|
|
22
22
|
- Whether a gateway token is configured (yes/no, not the token itself)
|
|
23
23
|
- OAuth client ID (when auth is enabled)
|
|
24
|
-
- Listening address and port (
|
|
24
|
+
- Listening address and port (HTTP mode)
|
|
25
25
|
- CORS origins configuration
|
|
26
26
|
|
|
27
27
|
### Connections (SSE/HTTP transport)
|
|
@@ -71,7 +71,7 @@ This is a safety net — the code avoids logging sensitive values in the first p
|
|
|
71
71
|
| Transport | Destination | Notes |
|
|
72
72
|
|-----------|-------------|-------|
|
|
73
73
|
| stdio | stderr | Cannot use stdout (reserved for MCP protocol) |
|
|
74
|
-
|
|
|
74
|
+
| HTTP | stderr | Same format, same destination |
|
|
75
75
|
| Docker | `docker logs openclaw-mcp` | stderr is captured by Docker's log driver |
|
|
76
76
|
|
|
77
77
|
## Docker Log Management
|
package/docs/threat-model.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
```
|
|
6
6
|
Claude (Desktop / Claude.ai)
|
|
7
7
|
|
|
|
8
|
-
| MCP Protocol (stdio or
|
|
8
|
+
| MCP Protocol (stdio or Streamable HTTP)
|
|
9
9
|
v
|
|
10
10
|
OpenClaw MCP Server (this project)
|
|
11
11
|
|
|
|
@@ -43,7 +43,7 @@ The MCP server is a **stateless proxy** — it translates MCP tool calls into Op
|
|
|
43
43
|
### Boundary 1: Claude <-> MCP Server
|
|
44
44
|
|
|
45
45
|
- **stdio transport (local):** Trusted — communication stays on the local machine. No authentication required.
|
|
46
|
-
- **
|
|
46
|
+
- **HTTP transport (remote):** Untrusted network — requires OAuth 2.1 authentication, HTTPS (via reverse proxy), and CORS restrictions.
|
|
47
47
|
|
|
48
48
|
### Boundary 2: MCP Server <-> OpenClaw Gateway
|
|
49
49
|
|
|
@@ -75,7 +75,7 @@ The MCP server is a **stateless proxy** — it translates MCP tool calls into Op
|
|
|
75
75
|
- Responses are parsed as JSON — no script execution
|
|
76
76
|
- Error messages from the gateway are sanitized before being returned to Claude
|
|
77
77
|
|
|
78
|
-
### 3. Network-Level Attacks (
|
|
78
|
+
### 3. Network-Level Attacks (HTTP transport)
|
|
79
79
|
|
|
80
80
|
**Risk:** Man-in-the-middle, replay attacks, unauthorized access.
|
|
81
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"mcpName": "io.github.freema/openclaw-mcp",
|
|
4
5
|
"description": "Model Context Protocol (MCP) server for OpenClaw AI assistant integration",
|
|
5
6
|
"author": "Tomáš Grasl <https://www.tomasgrasl.cz/>",
|
|
6
7
|
"license": "MIT",
|