openclaw-mcp 1.4.1 → 1.6.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 CHANGED
@@ -54,6 +54,8 @@ services:
54
54
  - MCP_CLIENT_ID=openclaw
55
55
  - MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET}
56
56
  - MCP_ISSUER_URL=${MCP_ISSUER_URL:-}
57
+ - MCP_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
58
+ - TRUST_PROXY=1
57
59
  - CORS_ORIGINS=https://claude.ai
58
60
  extra_hosts:
59
61
  - "host.docker.internal:host-gateway"
@@ -70,7 +72,9 @@ export OPENCLAW_GATEWAY_TOKEN=your-gateway-token
70
72
  docker compose up -d
71
73
  ```
72
74
 
73
- Then in Claude.ai add a custom MCP connector pointing to your server with `MCP_CLIENT_ID=openclaw` and your `MCP_CLIENT_SECRET`.
75
+ 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`.
76
+
77
+ > **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.
74
78
 
75
79
  > **Tip:** Pin a specific version instead of `latest` for production: `ghcr.io/freema/openclaw-mcp:1.1.0`
76
80
 
@@ -105,10 +109,14 @@ Add to your Claude Desktop config:
105
109
  AUTH_ENABLED=true MCP_CLIENT_ID=openclaw MCP_CLIENT_SECRET=your-secret \
106
110
  MCP_ISSUER_URL=https://mcp.your-domain.com \
107
111
  CORS_ORIGINS=https://claude.ai OPENCLAW_GATEWAY_TOKEN=your-gateway-token \
108
- npx openclaw-mcp --transport sse --port 3000
112
+ npx openclaw-mcp --transport http --port 3000
109
113
  ```
110
114
 
111
- > **Important:** When running behind a reverse proxy (Caddy, nginx, etc.), you **must** set `MCP_ISSUER_URL` (or `--issuer-url`) to your public HTTPS URL. Without this, OAuth metadata will advertise `http://localhost:3000` and clients will fail to authenticate.
115
+ > **Important:** When running behind a reverse proxy (Caddy, nginx, Traefik, Cloudflare Tunnel, etc.) you **must** set:
116
+ > - `MCP_ISSUER_URL` (or `--issuer-url`) to your public HTTPS URL — otherwise OAuth metadata advertises `http://localhost:3000` and clients fail to authenticate.
117
+ > - `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`.
118
+
119
+ > **Recommended:** Set `MCP_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback` so authorization codes can only be delivered to Claude's callbacks. Note the exact `/api/mcp/auth_callback` path — matching is exact, and getting it wrong fails OAuth with `Unregistered redirect_uri` (see [Troubleshooting](docs/deployment.md#invalid_request--unregistered-redirect_uri-on-authorize)).
112
120
 
113
121
  See [Installation Guide](docs/installation.md) for details.
114
122
 
@@ -250,7 +258,7 @@ export MCP_CLIENT_SECRET=$(openssl rand -hex 32)
250
258
 
251
259
  # Run with auth enabled
252
260
  AUTH_ENABLED=true MCP_CLIENT_ID=openclaw MCP_CLIENT_SECRET=$MCP_CLIENT_SECRET \
253
- openclaw-mcp --transport sse
261
+ openclaw-mcp --transport http
254
262
  ```
255
263
 
256
264
  Configure CORS to restrict access:
@@ -261,6 +269,36 @@ CORS_ORIGINS=https://claude.ai,https://your-app.com
261
269
 
262
270
  See [Configuration](docs/configuration.md) for all security options.
263
271
 
272
+ ## Migrating from SSE to HTTP transport
273
+
274
+ 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.
275
+
276
+ ### What changed
277
+
278
+ | Before | After |
279
+ |--------|-------|
280
+ | `--transport sse` | `--transport http` (recommended) |
281
+ | Primary endpoint: `GET /sse` | Primary endpoint: `POST/GET/DELETE /mcp` |
282
+ | Health: `"transport": "sse"` | Health: `"transport": "streamable-http"` |
283
+
284
+ ### Migration steps
285
+
286
+ 1. **CLI / Docker**: Replace `--transport sse` with `--transport http`
287
+ ```bash
288
+ # Before
289
+ openclaw-mcp --transport sse --port 3000
290
+ # After
291
+ openclaw-mcp --transport http --port 3000
292
+ ```
293
+
294
+ 2. **Claude.ai connector URL**: No change needed — Claude.ai already uses `/mcp` (Streamable HTTP)
295
+
296
+ 3. **Legacy clients**: The `/sse` and `/messages` endpoints still work. A deprecation warning is logged on each SSE connection.
297
+
298
+ 4. **Dockerfile ENTRYPOINT**: Updated automatically if using the official Docker image
299
+
300
+ > **Note:** `--transport sse` will continue to work as a deprecated alias. Both transports are served simultaneously regardless of which flag you use.
301
+
264
302
  ## Requirements
265
303
 
266
304
  - 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.4.1";
8
+ var SERVER_VERSION = "1.6.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: "Transport mode (stdio for local, sse for remote)",
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 SSE server",
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 SSE server",
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 (SSE mode)",
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/sse.ts
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";
@@ -1170,13 +1175,15 @@ var TOKEN_TTL_MS = 60 * 60 * 1e3;
1170
1175
  var AUTH_CODE_TTL_MS = 10 * 60 * 1e3;
1171
1176
  var REFRESH_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
1172
1177
  var REAPER_INTERVAL_MS = 5 * 60 * 1e3;
1173
- var ALLOW_ANY_REDIRECT = new Proxy([], {
1174
- get(target, prop) {
1175
- if (prop === "includes") return () => true;
1176
- if (prop === "length") return 1;
1177
- return Reflect.get(target, prop);
1178
+ var AllowAnyRedirectUris = class extends Array {
1179
+ includes(_searchElement, _fromIndex) {
1180
+ return true;
1178
1181
  }
1179
- });
1182
+ some(_predicate, _thisArg) {
1183
+ return true;
1184
+ }
1185
+ };
1186
+ var ALLOW_ANY_REDIRECT = new AllowAnyRedirectUris();
1180
1187
  var MAX_DYNAMIC_CLIENTS = 100;
1181
1188
  var OpenClawClientsStore = class {
1182
1189
  client;
@@ -1369,7 +1376,23 @@ var OpenClawAuthProvider = class {
1369
1376
  }
1370
1377
  };
1371
1378
 
1372
- // src/server/sse.ts
1379
+ // src/server/http.ts
1380
+ function parseTrustProxy(value) {
1381
+ if (value === void 0 || value === "") {
1382
+ return void 0;
1383
+ }
1384
+ const trimmed = value.trim();
1385
+ if (trimmed === "") {
1386
+ return void 0;
1387
+ }
1388
+ const lower = trimmed.toLowerCase();
1389
+ if (lower === "true") return true;
1390
+ if (lower === "false") return false;
1391
+ if (/^\d+$/.test(trimmed)) {
1392
+ return parseInt(trimmed, 10);
1393
+ }
1394
+ return trimmed;
1395
+ }
1373
1396
  function loadCorsConfig() {
1374
1397
  const corsOrigins = process.env.CORS_ORIGINS;
1375
1398
  if (!corsOrigins || corsOrigins === "*") {
@@ -1399,12 +1422,16 @@ function isOriginAllowed(origin, allowedOrigins) {
1399
1422
  return origin === allowed || origin === `https://${allowed}` || origin === `http://${allowed}`;
1400
1423
  });
1401
1424
  }
1402
- async function createSSEServer(config, deps2) {
1425
+ async function createHttpServer(config, deps2) {
1403
1426
  const authEnabled = !!config.authConfig?.clientId;
1404
1427
  const corsConfig = loadCorsConfig();
1405
- const sseSessions = /* @__PURE__ */ new Map();
1428
+ const legacySseSessions = /* @__PURE__ */ new Map();
1406
1429
  const streamableSessions = /* @__PURE__ */ new Map();
1407
1430
  const app = createMcpExpressApp({ host: config.host });
1431
+ if (config.trustProxy !== void 0) {
1432
+ app.set("trust proxy", config.trustProxy);
1433
+ log(`Trust proxy: ${JSON.stringify(config.trustProxy)}`);
1434
+ }
1408
1435
  app.use((req, res, next) => {
1409
1436
  if (!corsConfig.enabled) {
1410
1437
  next();
@@ -1450,7 +1477,8 @@ async function createSSEServer(config, deps2) {
1450
1477
  app.get("/health", (_req, res) => {
1451
1478
  res.json({
1452
1479
  status: "ok",
1453
- transport: "sse",
1480
+ transport: "streamable-http",
1481
+ legacySseSupported: true,
1454
1482
  auth: authEnabled
1455
1483
  });
1456
1484
  });
@@ -1463,19 +1491,20 @@ async function createSSEServer(config, deps2) {
1463
1491
  app.get(
1464
1492
  "/sse",
1465
1493
  ...withAuth(async (req, res) => {
1494
+ log("Legacy SSE transport is deprecated; prefer Streamable HTTP at /mcp");
1466
1495
  const transport = new SSEServerTransport("/messages", res);
1467
1496
  const server = createMcpServer(deps2);
1468
1497
  const sessionId = transport.sessionId;
1469
- sseSessions.set(sessionId, { transport, server });
1498
+ legacySseSessions.set(sessionId, { transport, server });
1470
1499
  log(`SSE session connected: ${sessionId}`);
1471
1500
  transport.onclose = () => {
1472
- sseSessions.delete(sessionId);
1501
+ legacySseSessions.delete(sessionId);
1473
1502
  log(`SSE session disconnected: ${sessionId}`);
1474
1503
  };
1475
1504
  try {
1476
1505
  await server.connect(transport);
1477
1506
  } catch (error) {
1478
- sseSessions.delete(sessionId);
1507
+ legacySseSessions.delete(sessionId);
1479
1508
  logError(`Failed to connect SSE session ${sessionId}`, error);
1480
1509
  }
1481
1510
  })
@@ -1484,7 +1513,7 @@ async function createSSEServer(config, deps2) {
1484
1513
  "/messages",
1485
1514
  ...withAuth(async (req, res) => {
1486
1515
  const sessionId = req.query.sessionId;
1487
- const session = sseSessions.get(sessionId);
1516
+ const session = legacySseSessions.get(sessionId);
1488
1517
  if (!session) {
1489
1518
  res.status(404).json({ error: "Session not found" });
1490
1519
  return;
@@ -1554,7 +1583,7 @@ async function createSSEServer(config, deps2) {
1554
1583
  app.post("/mcp", ...withAuth(handleStreamableRequest));
1555
1584
  app.delete("/mcp", ...withAuth(handleStreamableRequest));
1556
1585
  const httpServer = app.listen(config.port, config.host, () => {
1557
- log(`SSE server listening on ${config.host}:${config.port}`);
1586
+ log(`HTTP server listening on ${config.host}:${config.port}`);
1558
1587
  log(`Auth enabled: ${authEnabled}`);
1559
1588
  log(`CORS origins: ${corsConfig.enabled ? corsConfig.origins.join(", ") : "disabled"}`);
1560
1589
  if (authEnabled) {
@@ -1569,20 +1598,20 @@ async function createSSEServer(config, deps2) {
1569
1598
  }
1570
1599
  log("MCP Endpoints:");
1571
1600
  log(" GET /health - Health check (no auth)");
1572
- log(" GET /sse - Legacy SSE stream");
1573
- log(" POST /messages - Legacy SSE messages");
1574
- log(" ALL /mcp - Streamable HTTP");
1601
+ log(" ALL /mcp - Streamable HTTP (primary)");
1602
+ log(" GET /sse - Legacy SSE stream (deprecated)");
1603
+ log(" POST /messages - Legacy SSE messages (deprecated)");
1575
1604
  });
1576
1605
  const shutdown = async () => {
1577
- log("Shutting down SSE server...");
1578
- for (const [id, session] of sseSessions) {
1606
+ log("Shutting down HTTP server...");
1607
+ for (const [id, session] of legacySseSessions) {
1579
1608
  try {
1580
1609
  await session.server.close();
1581
1610
  } catch (error) {
1582
1611
  logError(`Error closing SSE session ${id}`, error);
1583
1612
  }
1584
1613
  }
1585
- sseSessions.clear();
1614
+ legacySseSessions.clear();
1586
1615
  for (const [id, session] of streamableSessions) {
1587
1616
  try {
1588
1617
  await session.server.close();
@@ -1592,7 +1621,7 @@ async function createSSEServer(config, deps2) {
1592
1621
  }
1593
1622
  streamableSessions.clear();
1594
1623
  httpServer.close(() => {
1595
- log("SSE server stopped");
1624
+ log("HTTP server stopped");
1596
1625
  process.exit(0);
1597
1626
  });
1598
1627
  setTimeout(() => {
@@ -1631,11 +1660,15 @@ async function main() {
1631
1660
  const defaultLabel = instance.isDefault ? " (default)" : "";
1632
1661
  log(`Instance "${instance.name}": ${instance.url}${defaultLabel}`);
1633
1662
  }
1634
- if (args.transport === "sse") {
1635
- const sseConfig = {
1663
+ if (args.transport === "sse" || args.transport === "http") {
1664
+ if (args.transport === "sse") {
1665
+ log("WARNING: --transport sse is deprecated; use --transport http instead");
1666
+ }
1667
+ const httpConfig = {
1636
1668
  port: args.port,
1637
1669
  host: args.host,
1638
- issuerUrl: args.issuerUrl
1670
+ issuerUrl: args.issuerUrl,
1671
+ trustProxy: parseTrustProxy(args.trustProxy)
1639
1672
  };
1640
1673
  if (args.authEnabled && args.clientId) {
1641
1674
  const clientIdRegex = /^[a-zA-Z0-9][a-zA-Z0-9_-]{2,63}$/;
@@ -1651,7 +1684,7 @@ async function main() {
1651
1684
  );
1652
1685
  process.exit(1);
1653
1686
  }
1654
- sseConfig.authConfig = {
1687
+ httpConfig.authConfig = {
1655
1688
  clientId: args.clientId,
1656
1689
  clientSecret: args.clientSecret,
1657
1690
  redirectUris: args.redirectUris,
@@ -1685,7 +1718,7 @@ async function main() {
1685
1718
  logError("AUTH_ENABLED=true but MCP_CLIENT_ID is not set. Refusing to start without auth.");
1686
1719
  process.exit(1);
1687
1720
  }
1688
- await createSSEServer(sseConfig, deps);
1721
+ await createHttpServer(httpConfig, deps);
1689
1722
  } else {
1690
1723
  const server = createMcpServer(deps);
1691
1724
  const transport = new StdioServerTransport();
@@ -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 (SSE transport)
103
+ ### Server Settings (HTTP transport)
104
104
 
105
- | Variable | Description | Default |
106
- | -------- | -------------------- | --------- |
107
- | `PORT` | SSE server port | `3000` |
108
- | `HOST` | SSE server host | `0.0.0.0` |
109
- | `DEBUG` | Enable debug logging | `false` |
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
 
@@ -157,6 +170,18 @@ When auth is enabled, the server exposes these OAuth 2.1 endpoints:
157
170
 
158
171
  Dynamic client registration is **disabled by default** — only the pre-configured client (from `MCP_CLIENT_ID` + `MCP_CLIENT_SECRET`) can authenticate. This prevents anyone who knows the server URL from self-registering and bypassing auth.
159
172
 
173
+ #### Redirect URI allow-list
174
+
175
+ `MCP_REDIRECT_URIS` restricts which `redirect_uri` values the `/authorize` endpoint accepts for the pre-configured client. When unset, any redirect URI is accepted (the client secret verified during token exchange remains the actual auth gate) — set it in production so authorization codes can only be delivered to callbacks you trust.
176
+
177
+ Claude.ai uses exactly this callback (note the `/api/mcp/auth_callback` path):
178
+
179
+ ```bash
180
+ MCP_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
181
+ ```
182
+
183
+ Matching is exact (scheme, host, path). A common mistake is registering `https://claude.ai/oauth/callback`, which does **not** match and makes Claude.ai fail with `Unregistered redirect_uri`. If you also connect other clients (e.g. MCP Inspector), append their callbacks to the comma-separated list — loopback callbacks (`http://localhost/...`, `http://127.0.0.1/...`) match on any port per RFC 8252.
184
+
160
185
  #### Cursor / Windsurf compatibility (dev only)
161
186
 
162
187
  Cursor and Windsurf only support MCP servers that expose OAuth 2.0 Dynamic Client Registration (RFC 7591). To let them connect, set `MCP_DANGEROUSLY_ALLOW_DCR=true`. The server will then advertise a `/register` endpoint and accept ad-hoc client registrations (kept in an in-memory FIFO store, capped at 100 entries).
@@ -24,6 +24,7 @@ services:
24
24
  - MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET:-}
25
25
  - MCP_ISSUER_URL=${MCP_ISSUER_URL:-}
26
26
  - MCP_REDIRECT_URIS=${MCP_REDIRECT_URIS:-}
27
+ - TRUST_PROXY=${TRUST_PROXY:-}
27
28
  - CORS_ORIGINS=${CORS_ORIGINS:-https://claude.ai}
28
29
  - NODE_ENV=production
29
30
  extra_hosts:
@@ -50,12 +51,18 @@ OPENCLAW_GATEWAY_TOKEN=your-gateway-token
50
51
  MCP_CLIENT_ID=openclaw
51
52
  MCP_CLIENT_SECRET=your-client-secret
52
53
 
53
- # Enable OAuth (required for production SSE)
54
+ # Enable OAuth (required for production HTTP transport)
54
55
  AUTH_ENABLED=true
55
56
 
56
57
  # Public URL (required when behind a reverse proxy)
57
58
  MCP_ISSUER_URL=https://mcp.your-domain.com
58
59
 
60
+ # Trust the reverse proxy's X-Forwarded-For (required behind a reverse proxy)
61
+ TRUST_PROXY=1
62
+
63
+ # Allowed OAuth redirect URIs — Claude.ai callbacks (recommended for production)
64
+ MCP_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
65
+
59
66
  # Allowed CORS origins
60
67
  CORS_ORIGINS=https://claude.ai
61
68
  ```
@@ -78,7 +85,8 @@ docker compose up -d
78
85
  - [ ] `MCP_CLIENT_ID` is valid (3–64 chars, alphanumeric/dashes/underscores)
79
86
  - [ ] `MCP_CLIENT_SECRET` generated securely (`openssl rand -hex 32`, min 32 chars)
80
87
  - [ ] `MCP_ISSUER_URL` set to public HTTPS URL (when behind reverse proxy)
81
- - [ ] `MCP_REDIRECT_URIS` restricted to known callback URLs
88
+ - [ ] `TRUST_PROXY` set to the right hop count / CIDR (when behind reverse proxy)
89
+ - [ ] `MCP_REDIRECT_URIS` restricted to known callback URLs (for Claude.ai: `https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback`)
82
90
  - [ ] CORS restricted to known origins (`CORS_ORIGINS=https://claude.ai`)
83
91
  - [ ] `OPENCLAW_GATEWAY_TOKEN` set for gateway authentication
84
92
  - [ ] Dynamic client registration is disabled (default — no `/register` endpoint)
@@ -88,7 +96,9 @@ docker compose up -d
88
96
 
89
97
  The MCP bridge must be served over HTTPS for production use. Use a reverse proxy that handles TLS termination.
90
98
 
91
- > **Important:** You **must** set `MCP_ISSUER_URL` to your public HTTPS URL. Without this, OAuth metadata endpoints will advertise `http://localhost:3000` and MCP clients (including Claude.ai) will fail to authenticate with the error: `Protected resource http://localhost:3000/mcp does not match expected https://your-domain.com/mcp`.
99
+ > **Important:** When running behind a reverse proxy you **must** set both:
100
+ > - `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`.
101
+ > - `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
102
 
93
103
  ### Caddy (recommended)
94
104
 
@@ -122,6 +132,7 @@ services:
122
132
  - "3000"
123
133
  environment:
124
134
  - MCP_ISSUER_URL=https://mcp.your-domain.com
135
+ - TRUST_PROXY=1
125
136
  # ... other env vars
126
137
 
127
138
  volumes:
@@ -197,6 +208,26 @@ The OpenClaw gateway's HTTP chat completions endpoint is disabled by default. En
197
208
 
198
209
  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
210
 
211
+ ### `POST /` or `GET /` returns 404 after OAuth succeeds
212
+
213
+ 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`.
214
+
215
+ ### `invalid_request` / `Unregistered redirect_uri` on `/authorize`
216
+
217
+ The `redirect_uri` the client sent is not on the server's allow-list. Two common causes:
218
+
219
+ 1. **`MCP_REDIRECT_URIS` is set but doesn't contain the client's exact callback.** Claude.ai uses `https://claude.ai/api/mcp/auth_callback` (and `https://claude.com/api/mcp/auth_callback`) — a similar-looking entry like `https://claude.ai/oauth/callback` does **not** match. Matching is exact on scheme, host, and path; only loopback callbacks (`localhost`, `127.0.0.1`, `[::1]`) get port relaxation per RFC 8252.
220
+
221
+ 2. **You're on bridge ≤ 1.5.0 with `MCP_REDIRECT_URIS` unset.** The allow-any fallback in those versions was silently broken by a change in MCP SDK 1.29 (the SDK switched its membership check from `.includes()` to `.some()`), so *every* redirect_uri was rejected — fresh `npx openclaw-mcp@latest` installs picked the new SDK up automatically. Upgrade to bridge ≥ 1.6.0, or set `MCP_REDIRECT_URIS` explicitly (recommended for production anyway):
222
+
223
+ ```bash
224
+ MCP_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
225
+ ```
226
+
227
+ ### `ValidationError: ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` on `/token`
228
+
229
+ 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).
230
+
200
231
  ### `fetch failed` / MCP bridge can't reach gateway
201
232
 
202
233
  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.
@@ -48,7 +48,7 @@ src/
48
48
  ├── mcp/
49
49
  │ └── tools/ # MCP tool implementations
50
50
  ├── openclaw/ # OpenClaw API client
51
- ├── server/ # SSE server (for remote access)
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
@@ -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 SSE transport and OAuth 2.1 authentication.
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 sse --port 3000
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 SSE server [default: 3000]
97
- --host Host for SSE server [default: "0.0.0.0"]
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 SSE)
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 (SSE mode)
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
- | SSE/HTTP | stderr | Same format, same destination |
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
@@ -5,7 +5,7 @@
5
5
  ```
6
6
  Claude (Desktop / Claude.ai)
7
7
  |
8
- | MCP Protocol (stdio or SSE/Streamable HTTP)
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
- - **SSE/HTTP transport (remote):** Untrusted network — requires OAuth 2.1 authentication, HTTPS (via reverse proxy), and CORS restrictions.
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 (SSE transport)
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,6 @@
1
1
  {
2
2
  "name": "openclaw-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "mcpName": "io.github.freema/openclaw-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for OpenClaw AI assistant integration",
6
6
  "author": "Tomáš Grasl <https://www.tomasgrasl.cz/>",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
44
  "yargs": "^17.7.2"
45
45
  },
46
46
  "devDependencies": {