openclaw-mcp 1.3.0 → 1.4.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
@@ -47,6 +47,7 @@ services:
47
47
  environment:
48
48
  - OPENCLAW_URL=http://host.docker.internal:18789
49
49
  - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
50
+ - OPENCLAW_MODEL=openclaw
50
51
  - AUTH_ENABLED=true
51
52
  - MCP_CLIENT_ID=openclaw
52
53
  - MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET}
@@ -88,6 +89,7 @@ Add to your Claude Desktop config:
88
89
  "env": {
89
90
  "OPENCLAW_URL": "http://127.0.0.1:18789",
90
91
  "OPENCLAW_GATEWAY_TOKEN": "your-gateway-token",
92
+ "OPENCLAW_MODEL": "openclaw",
91
93
  "OPENCLAW_TIMEOUT_MS": "300000"
92
94
  }
93
95
  }
@@ -141,6 +143,7 @@ See [Installation Guide](docs/installation.md) for details.
141
143
  |------|-------------|
142
144
  | `openclaw_chat` | Send messages to OpenClaw and get responses |
143
145
  | `openclaw_status` | Check OpenClaw gateway health |
146
+ | `openclaw_instances` | List all configured OpenClaw instances |
144
147
 
145
148
  ### Async Tools (for long-running operations)
146
149
 
@@ -151,6 +154,80 @@ See [Installation Guide](docs/installation.md) for details.
151
154
  | `openclaw_task_list` | List all tasks with filtering |
152
155
  | `openclaw_task_cancel` | Cancel a pending task |
153
156
 
157
+ ## Multi-Instance Mode
158
+
159
+ Orchestrate multiple OpenClaw gateways from a single MCP server. One bridge, many claws — route requests to prod, staging, dev, or whatever you name them (lobster-supreme and the-claw-abides are perfectly valid names).
160
+
161
+ ```
162
+ ┌──────────────────────────────────────────────────────────────────────┐
163
+ │ Claude.ai / Claude Desktop │
164
+ │ (MCP Client) │
165
+ └──────────────────────┬───────────────────────────────────────────────┘
166
+
167
+
168
+ ┌──────────────────────────────────────────────────────────────────────┐
169
+ │ OpenClaw MCP Bridge Server │
170
+ │ │
171
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
172
+ │ │ Instance │ │ Instance │ │ Instance │ │
173
+ │ │ Registry │ │ Resolver │ │ Validator │ │
174
+ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
175
+ │ │ │ │ │
176
+ │ ┌──────┴─────────────────┴──────────────────┴───────┐ │
177
+ │ │ Per-Instance OpenClaw Clients │ │
178
+ │ │ (separate auth, timeout, URL per instance) │ │
179
+ │ └────────┬──────────────┬──────────────┬────────────┘ │
180
+ └───────────┼──────────────┼──────────────┼────────────────────────────┘
181
+ │ │ │
182
+ ▼ ▼ ▼
183
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
184
+ │ 🦞 prod │ │ 🦞 staging │ │ 🦞 dev │
185
+ │ (default) │ │ │ │ │
186
+ │ :18789 │ │ :18789 │ │ :18789 │
187
+ │ OpenClaw GW │ │ OpenClaw GW │ │ OpenClaw GW │
188
+ └──────────────┘ └──────────────┘ └──────────────┘
189
+ ```
190
+
191
+ ### Setup
192
+
193
+ ```bash
194
+ OPENCLAW_INSTANCES='[
195
+ {"name": "prod", "url": "http://prod:18789", "token": "tok1", "default": true},
196
+ {"name": "staging", "url": "http://staging:18789", "token": "tok2"},
197
+ {"name": "dev", "url": "http://dev:18789", "token": "tok3"}
198
+ ]'
199
+ ```
200
+
201
+ ### Usage
202
+
203
+ All tools accept an optional `instance` parameter to target a specific gateway:
204
+
205
+ ```
206
+ # Chat with staging instance
207
+ openclaw_chat message="Deploy status?" instance="staging"
208
+
209
+ # Check health of prod
210
+ openclaw_status instance="prod"
211
+
212
+ # List all configured instances
213
+ openclaw_instances
214
+
215
+ # Async task targeting dev
216
+ openclaw_chat_async message="Run tests" instance="dev"
217
+ ```
218
+
219
+ When `instance` is omitted, the default instance is used. Each instance has its own auth token, timeout, and URL — fully isolated.
220
+
221
+ ### Key Features
222
+
223
+ - **Zero-migration upgrade** — existing single-instance deployments work without any config change
224
+ - **Per-instance isolation** — separate auth tokens, timeouts, and URLs
225
+ - **Dynamic routing** — Claude picks the right instance per request
226
+ - **Task tracking** — async tasks remember which instance they target
227
+ - **Security** — tokens are never exposed via `openclaw_instances`
228
+
229
+ See [Configuration — Multi-Instance Mode](docs/configuration.md#multi-instance-mode) for the full reference.
230
+
154
231
  ## Documentation
155
232
 
156
233
  - [Installation](docs/installation.md) — Setup for Claude Desktop & Claude.ai
package/dist/index.js CHANGED
@@ -5,11 +5,13 @@ 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.3.0";
8
+ var SERVER_VERSION = "1.4.0";
9
9
  var DEFAULT_OPENCLAW_URL = "http://127.0.0.1:18789";
10
+ var DEFAULT_MODEL = "openclaw";
10
11
  var SERVER_ICON_SVG_BASE64 = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgZmlsbD0ibm9uZSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzFhMWEyZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzE2MjEzZSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjbGF3IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZmYzMzMzIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjY2MwMDAwIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIyNCIgZmlsbD0idXJsKCNiZykiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NCA2NCkiIHN0cm9rZT0idXJsKCNjbGF3KSIgc3Ryb2tlLXdpZHRoPSI3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiPjxwYXRoIGQ9Ik0tMjggLTM4YzAgMCAtMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0tMTIgLTQwYzAgMCAtNiAyMiA0IDM0Ii8+PHBhdGggZD0iTTI4IC0zOGMwIDAgMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0xMiAtNDBjMCAwIDYgMjIgLTQgMzQiLz48Y2lyY2xlIGN4PSIwIiBjeT0iMTAiIHI9IjIwIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNLTEwIDR2LTQiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Ik0xMCA0di00IiBzdHJva2Utd2lkdGg9IjQiLz48cGF0aCBkPSJNLTggMjBjNCA2IDEyIDYgMTYgMCIgc3Ryb2tlLXdpZHRoPSIzIi8+PC9nPjwvc3ZnPg==";
11
12
 
12
13
  // src/utils/logger.ts
14
+ var debugEnabled = false;
13
15
  var SENSITIVE_PATTERNS = [
14
16
  /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
15
17
  /api[_-]?key["\s:=]+[A-Za-z0-9\-._~+/]{8,}/gi,
@@ -27,6 +29,19 @@ function sanitizeLogMessage(message) {
27
29
  function log(message) {
28
30
  console.error(`[openclaw-mcp] ${sanitizeLogMessage(message)}`);
29
31
  }
32
+ function setDebugEnabled(enabled) {
33
+ debugEnabled = enabled;
34
+ }
35
+ function isDebugEnabled() {
36
+ return debugEnabled;
37
+ }
38
+ function logDebug(messageOrFactory) {
39
+ if (!debugEnabled) {
40
+ return;
41
+ }
42
+ const message = typeof messageOrFactory === "function" ? messageOrFactory() : messageOrFactory;
43
+ console.error(`[openclaw-mcp] DEBUG: ${sanitizeLogMessage(message)}`);
44
+ }
30
45
  function logError(message, error) {
31
46
  console.error(`[openclaw-mcp] ERROR: ${sanitizeLogMessage(message)}`);
32
47
  if (error) {
@@ -51,6 +66,11 @@ function parseArguments(version) {
51
66
  type: "string",
52
67
  description: "Bearer token for OpenClaw gateway authentication",
53
68
  default: process.env.OPENCLAW_GATEWAY_TOKEN || void 0
69
+ }).option("model", {
70
+ alias: "m",
71
+ type: "string",
72
+ description: "Model name for chat completions",
73
+ default: process.env.OPENCLAW_MODEL || DEFAULT_MODEL
54
74
  }).option("transport", {
55
75
  alias: "t",
56
76
  type: "string",
@@ -70,6 +90,10 @@ function parseArguments(version) {
70
90
  type: "number",
71
91
  description: "Request timeout in milliseconds",
72
92
  default: parseInt(process.env.OPENCLAW_TIMEOUT_MS || "120000", 10)
93
+ }).option("debug", {
94
+ type: "boolean",
95
+ description: "Enable debug logging",
96
+ default: process.env.DEBUG === "true" || process.env.NODE_ENV === "development"
73
97
  }).option("auth", {
74
98
  type: "boolean",
75
99
  description: "Enable OAuth authentication (SSE mode)",
@@ -90,6 +114,10 @@ function parseArguments(version) {
90
114
  type: "string",
91
115
  description: "Allowed OAuth redirect URIs (comma-separated)",
92
116
  default: process.env.MCP_REDIRECT_URIS || void 0
117
+ }).option("allow-dcr", {
118
+ type: "boolean",
119
+ description: "Allow OAuth Dynamic Client Registration (Cursor/Windsurf compatibility, dev-only)",
120
+ default: process.env.MCP_DANGEROUSLY_ALLOW_DCR === "true"
93
121
  }).help().parseSync();
94
122
  let instances;
95
123
  const instancesEnv = process.env.OPENCLAW_INSTANCES;
@@ -133,15 +161,18 @@ function parseArguments(version) {
133
161
  return {
134
162
  openclawUrl: argv["openclaw-url"],
135
163
  gatewayToken: argv["gateway-token"],
164
+ model: argv.model,
136
165
  transport: argv.transport,
137
166
  port: argv.port,
138
167
  host: argv.host,
139
168
  timeout: argv.timeout,
169
+ debug: argv.debug,
140
170
  authEnabled: argv.auth,
141
171
  clientId: argv["client-id"],
142
172
  clientSecret: argv["client-secret"],
143
173
  issuerUrl: argv["issuer-url"],
144
174
  redirectUris: argv["redirect-uris"] ? argv["redirect-uris"].split(",").map((s) => s.trim()).filter(Boolean) : void 0,
175
+ allowDcr: argv["allow-dcr"],
145
176
  instances
146
177
  };
147
178
  }
@@ -171,14 +202,17 @@ var OpenClawApiError = class extends OpenClawError {
171
202
  // src/openclaw/client.ts
172
203
  var DEFAULT_TIMEOUT_MS = 12e4;
173
204
  var MAX_RESPONSE_SIZE_BYTES = 10 * 1024 * 1024;
205
+ var MAX_DEBUG_BODY_LENGTH = 4096;
174
206
  var OpenClawClient = class {
175
207
  baseUrl;
176
208
  gatewayToken;
177
209
  timeoutMs;
178
- constructor(baseUrl, gatewayToken, timeoutMs = DEFAULT_TIMEOUT_MS) {
210
+ model;
211
+ constructor(baseUrl, gatewayToken, timeoutMs = DEFAULT_TIMEOUT_MS, model = "openclaw") {
179
212
  this.baseUrl = baseUrl.replace(/\/$/, "");
180
213
  this.gatewayToken = gatewayToken;
181
214
  this.timeoutMs = timeoutMs;
215
+ this.model = model;
182
216
  }
183
217
  buildHeaders() {
184
218
  const headers = {
@@ -189,8 +223,16 @@ var OpenClawClient = class {
189
223
  }
190
224
  return headers;
191
225
  }
226
+ truncateForLog(value) {
227
+ if (value.length <= MAX_DEBUG_BODY_LENGTH) return value;
228
+ return value.slice(0, MAX_DEBUG_BODY_LENGTH) + `... (truncated, ${value.length} chars total)`;
229
+ }
192
230
  async request(path, options = {}) {
193
231
  const url = `${this.baseUrl}${path}`;
232
+ logDebug(() => `Request: ${options.method ?? "GET"} ${url}`);
233
+ if (options.body) {
234
+ logDebug(() => `Request body: ${this.truncateForLog(options.body)}`);
235
+ }
194
236
  const controller = new AbortController();
195
237
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
196
238
  try {
@@ -203,11 +245,23 @@ var OpenClawClient = class {
203
245
  }
204
246
  });
205
247
  if (!response.ok) {
248
+ if (isDebugEnabled()) {
249
+ const contentLength2 = response.headers.get("content-length");
250
+ if (!contentLength2 || parseInt(contentLength2, 10) <= MAX_RESPONSE_SIZE_BYTES) {
251
+ const errorBody = await response.text();
252
+ if (errorBody.length <= MAX_RESPONSE_SIZE_BYTES) {
253
+ logDebug(
254
+ () => `Response error (${response.status}): ${this.truncateForLog(errorBody)}`
255
+ );
256
+ }
257
+ }
258
+ }
206
259
  throw new OpenClawApiError(
207
260
  `API request failed: ${response.status} ${response.statusText}`,
208
261
  response.status
209
262
  );
210
263
  }
264
+ logDebug(() => `Response: ${response.status} ${response.statusText}`);
211
265
  const contentLength = response.headers.get("content-length");
212
266
  if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE_BYTES) {
213
267
  throw new OpenClawApiError("Response exceeds maximum allowed size (10MB)", 413);
@@ -276,7 +330,7 @@ var OpenClawClient = class {
276
330
  */
277
331
  async chat(message, sessionId) {
278
332
  const body = {
279
- model: "claude-opus-4-5",
333
+ model: this.model,
280
334
  messages: [{ role: "user", content: message }],
281
335
  max_tokens: 4096
282
336
  };
@@ -306,7 +360,7 @@ var INSTANCE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
306
360
  var InstanceRegistry = class {
307
361
  instances = /* @__PURE__ */ new Map();
308
362
  defaultName;
309
- constructor(configs) {
363
+ constructor(configs, model) {
310
364
  if (configs.length === 0) {
311
365
  throw new Error("At least one OpenClaw instance must be configured");
312
366
  }
@@ -343,7 +397,7 @@ var InstanceRegistry = class {
343
397
  }
344
398
  explicitDefault = config.name;
345
399
  }
346
- const client = new OpenClawClient(config.url, config.token, config.timeout);
400
+ const client = new OpenClawClient(config.url, config.token, config.timeout, model);
347
401
  this.instances.set(config.name, { config, client });
348
402
  }
349
403
  this.defaultName = explicitDefault ?? configs[0].name;
@@ -1123,8 +1177,15 @@ var ALLOW_ANY_REDIRECT = new Proxy([], {
1123
1177
  return Reflect.get(target, prop);
1124
1178
  }
1125
1179
  });
1180
+ var MAX_DYNAMIC_CLIENTS = 100;
1126
1181
  var OpenClawClientsStore = class {
1127
1182
  client;
1183
+ dynamicClients = /* @__PURE__ */ new Map();
1184
+ // Assigned conditionally in the constructor. The MCP SDK probes
1185
+ // `clientsStore.registerClient` at router-setup time and only advertises
1186
+ // `/register` when the property is defined, so we must NOT define a no-op
1187
+ // method on the prototype — it has to be instance-level and gated on config.
1188
+ registerClient;
1128
1189
  constructor(config) {
1129
1190
  if (config.clientId && config.clientSecret) {
1130
1191
  const redirectUris = config.redirectUris && config.redirectUris.length > 0 ? config.redirectUris : ALLOW_ANY_REDIRECT;
@@ -1139,16 +1200,25 @@ var OpenClawClientsStore = class {
1139
1200
  client_id_issued_at: Math.floor(Date.now() / 1e3)
1140
1201
  };
1141
1202
  }
1203
+ if (config.allowDynamicRegistration) {
1204
+ this.registerClient = (client) => {
1205
+ if (this.dynamicClients.size >= MAX_DYNAMIC_CLIENTS) {
1206
+ const oldestKey = this.dynamicClients.keys().next().value;
1207
+ if (oldestKey !== void 0) {
1208
+ this.dynamicClients.delete(oldestKey);
1209
+ }
1210
+ }
1211
+ this.dynamicClients.set(client.client_id, client);
1212
+ return client;
1213
+ };
1214
+ }
1142
1215
  }
1143
1216
  async getClient(clientId) {
1144
1217
  if (this.client && this.client.client_id === clientId) {
1145
1218
  return this.client;
1146
1219
  }
1147
- return void 0;
1220
+ return this.dynamicClients.get(clientId);
1148
1221
  }
1149
- // No registerClient — dynamic client registration is intentionally disabled.
1150
- // Only the pre-configured client from MCP_CLIENT_ID + MCP_CLIENT_SECRET can authenticate.
1151
- // This prevents anyone who knows the server URL from self-registering and bypassing auth.
1152
1222
  };
1153
1223
  var OpenClawAuthProvider = class {
1154
1224
  clientsStore;
@@ -1287,9 +1357,15 @@ var OpenClawAuthProvider = class {
1287
1357
  resource: tokenData.resource
1288
1358
  };
1289
1359
  }
1290
- async revokeToken(_client, request) {
1291
- this.tokens.delete(request.token);
1292
- this.refreshTokens.delete(request.token);
1360
+ async revokeToken(client, request) {
1361
+ const tokenData = this.tokens.get(request.token);
1362
+ if (tokenData && tokenData.clientId === client.client_id) {
1363
+ this.tokens.delete(request.token);
1364
+ }
1365
+ const refreshData = this.refreshTokens.get(request.token);
1366
+ if (refreshData && refreshData.clientId === client.client_id) {
1367
+ this.refreshTokens.delete(request.token);
1368
+ }
1293
1369
  }
1294
1370
  };
1295
1371
 
@@ -1530,7 +1606,14 @@ async function createSSEServer(config, deps2) {
1530
1606
 
1531
1607
  // src/index.ts
1532
1608
  var args = parseArguments(SERVER_VERSION);
1533
- var registry = new InstanceRegistry(args.instances);
1609
+ setDebugEnabled(args.debug);
1610
+ var trimmedModel = args.model.trim();
1611
+ if (!trimmedModel) {
1612
+ logError('OPENCLAW_MODEL / --model must be a non-empty string. Default is "openclaw".');
1613
+ process.exit(1);
1614
+ }
1615
+ args.model = trimmedModel;
1616
+ var registry = new InstanceRegistry(args.instances, args.model);
1534
1617
  var deps = {
1535
1618
  registry,
1536
1619
  serverName: SERVER_NAME,
@@ -1538,8 +1621,12 @@ var deps = {
1538
1621
  };
1539
1622
  async function main() {
1540
1623
  log(`Starting ${SERVER_NAME} v${SERVER_VERSION}`);
1624
+ log(`Model: ${args.model}`);
1541
1625
  log(`Transport: ${args.transport}`);
1542
1626
  log(`Request timeout: ${args.timeout}ms`);
1627
+ if (args.debug) {
1628
+ log("Debug logging: enabled");
1629
+ }
1543
1630
  for (const instance of registry.list()) {
1544
1631
  const defaultLabel = instance.isDefault ? " (default)" : "";
1545
1632
  log(`Instance "${instance.name}": ${instance.url}${defaultLabel}`);
@@ -1567,7 +1654,8 @@ async function main() {
1567
1654
  sseConfig.authConfig = {
1568
1655
  clientId: args.clientId,
1569
1656
  clientSecret: args.clientSecret,
1570
- redirectUris: args.redirectUris
1657
+ redirectUris: args.redirectUris,
1658
+ allowDynamicRegistration: args.allowDcr
1571
1659
  };
1572
1660
  log(`OAuth client ID: ${args.clientId}`);
1573
1661
  if (!args.redirectUris || args.redirectUris.length === 0) {
@@ -1575,6 +1663,24 @@ async function main() {
1575
1663
  "WARNING: MCP_REDIRECT_URIS not set \u2014 any redirect_uri will be accepted. Set MCP_REDIRECT_URIS for production."
1576
1664
  );
1577
1665
  }
1666
+ if (args.allowDcr) {
1667
+ const isLoopback = args.host === "127.0.0.1" || args.host === "localhost" || args.host === "::1";
1668
+ const publicOptIn = process.env.MCP_DANGEROUSLY_ALLOW_DCR_PUBLIC === "true";
1669
+ if (!isLoopback && !publicOptIn) {
1670
+ logError(
1671
+ `MCP_DANGEROUSLY_ALLOW_DCR=true is set but HOST="${args.host}" is not loopback. Dynamic Client Registration combined with a publicly reachable bind allows anyone on the network to obtain a token. Bind to 127.0.0.1, or set MCP_DANGEROUSLY_ALLOW_DCR_PUBLIC=true to override. Refusing to start.`
1672
+ );
1673
+ process.exit(1);
1674
+ }
1675
+ log(
1676
+ "WARNING: MCP_DANGEROUSLY_ALLOW_DCR is enabled \u2014 OAuth Dynamic Client Registration is open. Any client that can reach this server may self-register and obtain tokens. Use for local development only."
1677
+ );
1678
+ if (publicOptIn) {
1679
+ log(
1680
+ "WARNING: MCP_DANGEROUSLY_ALLOW_DCR_PUBLIC=true \u2014 DCR is exposed on a non-loopback bind. You have explicitly accepted the risk."
1681
+ );
1682
+ }
1683
+ }
1578
1684
  } else if (args.authEnabled && !args.clientId) {
1579
1685
  logError("AUTH_ENABLED=true but MCP_CLIENT_ID is not set. Refusing to start without auth.");
1580
1686
  process.exit(1);
@@ -11,11 +11,27 @@ All configuration can be done via environment variables. Copy `.env.example` to
11
11
  | `OPENCLAW_URL` | OpenClaw gateway URL | `http://127.0.0.1:18789` |
12
12
  | `OPENCLAW_GATEWAY_TOKEN` | Bearer token for gateway authentication | (none) |
13
13
  | `OPENCLAW_TIMEOUT_MS` | Request timeout in milliseconds | `120000` (2 min) |
14
+ | `OPENCLAW_MODEL` | Model name for chat completions | `openclaw` |
14
15
 
15
16
  ### Multi-Instance Mode
16
17
 
17
18
  Orchestrate multiple OpenClaw gateways from a single MCP server. Set `OPENCLAW_INSTANCES` as a JSON array — when present, it takes precedence over `OPENCLAW_URL` / `OPENCLAW_GATEWAY_TOKEN`.
18
19
 
20
+ ```
21
+ ┌─────────────────┐
22
+ │ Claude Client │
23
+ └────────┬────────┘
24
+
25
+ ┌─────────────▼─────────────┐
26
+ │ OpenClaw MCP Bridge │
27
+ │ │
28
+ │ instance="prod" ──────────────► OpenClaw GW (prod)
29
+ │ instance="staging" ────────────► OpenClaw GW (staging)
30
+ │ instance="dev" ───────────────► OpenClaw GW (dev)
31
+ │ (no instance) ──► default ──► OpenClaw GW (prod)
32
+ └────────────────────────────┘
33
+ ```
34
+
19
35
  | Variable | Description | Default |
20
36
  | -------------------- | ------------------------------ | ----------------------------- |
21
37
  | `OPENCLAW_INSTANCES` | JSON array of instance configs | (none — single-instance mode) |
@@ -45,11 +61,44 @@ Each instance object supports:
45
61
  All gateway-facing tools (`openclaw_chat`, `openclaw_status`, `openclaw_chat_async`) accept an optional `instance` parameter. When omitted, the default instance is used.
46
62
 
47
63
  ```
64
+ # Target a specific instance
48
65
  openclaw_chat message="Hello" instance="staging"
49
- openclaw_instances # list all available instances
66
+
67
+ # Check health of a specific gateway
68
+ openclaw_status instance="prod"
69
+
70
+ # List all available instances (names, URLs, default — tokens are never exposed)
71
+ openclaw_instances
72
+
73
+ # Async tasks also support instance targeting
74
+ openclaw_chat_async message="Run migration" instance="dev"
75
+
76
+ # Filter task list by instance
77
+ openclaw_task_list instance="staging"
78
+ ```
79
+
80
+ **How instance resolution works:**
81
+
82
+ 1. If `instance` parameter is provided → use that instance
83
+ 2. If `instance` is omitted → use the instance marked as `default`
84
+ 3. If no instance is marked as default → the first instance in the array is used
85
+
86
+ Each instance gets its own isolated HTTP client with independent auth token, timeout, and base URL. Async tasks store the target instance ID so results are always routed correctly.
87
+
88
+ **Docker Compose with multi-instance:**
89
+
90
+ ```yaml
91
+ services:
92
+ mcp-bridge:
93
+ image: ghcr.io/freema/openclaw-mcp:latest
94
+ environment:
95
+ - OPENCLAW_INSTANCES=[{"name":"prod","url":"http://prod-gw:18789","token":"tok1","default":true},{"name":"staging","url":"http://staging-gw:18789","token":"tok2"}]
96
+ - AUTH_ENABLED=true
97
+ - MCP_CLIENT_ID=openclaw
98
+ - MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET}
50
99
  ```
51
100
 
52
- **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.
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.
53
102
 
54
103
  ### Server Settings (SSE transport)
55
104
 
@@ -84,10 +133,12 @@ The server uses the MCP SDK's built-in OAuth 2.1 server with authorization code
84
133
  | `MCP_CLIENT_SECRET` | OAuth client secret | When auth enabled |
85
134
  | `MCP_ISSUER_URL` | OAuth issuer URL override (e.g., `https://mcp.example.com`) | When behind HTTPS proxy |
86
135
  | `MCP_REDIRECT_URIS` | Allowed redirect URIs (comma-separated) | Recommended for production |
136
+ | `MCP_DANGEROUSLY_ALLOW_DCR` | Enable Dynamic Client Registration (`true`/`false`) | Dev only (see below) |
137
+ | `MCP_DANGEROUSLY_ALLOW_DCR_PUBLIC` | Escape hatch to allow DCR on non-loopback binds | Never, unless you mean it |
87
138
 
88
139
  **Client ID validation rules:**
89
140
 
90
- - 364 characters
141
+ - 3-64 characters
91
142
  - Alphanumeric, dashes, underscores only
92
143
  - Must start with a letter or digit
93
144
 
@@ -104,4 +155,14 @@ When auth is enabled, the server exposes these OAuth 2.1 endpoints:
104
155
  - `POST /token` — Token exchange (requires client_secret)
105
156
  - `POST /revoke` — Token revocation
106
157
 
107
- Dynamic client registration is **disabled** — 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.
158
+ 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
+
160
+ #### Cursor / Windsurf compatibility (dev only)
161
+
162
+ 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).
163
+
164
+ `MCP_CLIENT_ID` and `MCP_CLIENT_SECRET` are still required — DCR augments the pre-configured client, it does not replace it. If you are only running Cursor locally you can use any valid values for them; they simply remain unused.
165
+
166
+ **This is dev-only.** With DCR enabled alongside the server's auto-approve authorization flow, any client that can reach the server can register itself and obtain a token. To prevent accidental exposure the server refuses to start when `MCP_DANGEROUSLY_ALLOW_DCR=true` and `HOST` is not loopback (`127.0.0.1`, `localhost`, or `::1`). If you genuinely need DCR on a non-loopback bind (e.g., inside a trusted private network), also set `MCP_DANGEROUSLY_ALLOW_DCR_PUBLIC=true`.
167
+
168
+ For production with Claude.ai, keep DCR disabled and use the pre-configured `MCP_CLIENT_ID` / `MCP_CLIENT_SECRET`.
@@ -17,6 +17,8 @@ services:
17
17
  environment:
18
18
  - OPENCLAW_URL=${OPENCLAW_URL:-http://host.docker.internal:18789}
19
19
  - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-}
20
+ - OPENCLAW_MODEL=${OPENCLAW_MODEL:-openclaw}
21
+ - DEBUG=${DEBUG:-false}
20
22
  - AUTH_ENABLED=${AUTH_ENABLED:-true}
21
23
  - MCP_CLIENT_ID=${MCP_CLIENT_ID:-openclaw}
22
24
  - MCP_CLIENT_SECRET=${MCP_CLIENT_SECRET:-}
@@ -169,8 +171,24 @@ The MCP bridge communicates with the OpenClaw gateway via its OpenAI-compatible
169
171
 
170
172
  Without this, the MCP bridge will receive `405 Method Not Allowed` from the gateway.
171
173
 
174
+ ## Bridge / Gateway Compatibility
175
+
176
+ | MCP Bridge | Gateway | Result |
177
+ |------------|---------|--------|
178
+ | ≤ 1.2.2 | ≥ 2026.3.24 | `400 Bad Request` — bridge sends `model: "claude-opus-4-5"`, gateway rejects it |
179
+ | ≥ 1.3.0 | ≥ 2026.3.24 | Works — bridge defaults to `model: "openclaw"` |
180
+ | ≥ 1.3.0 | older | Works — set `OPENCLAW_MODEL` to whatever the older gateway expects |
181
+
182
+ If you're running a non-standard gateway setup with custom agent routing, set `OPENCLAW_MODEL=openclaw/<agentId>` to match your configuration.
183
+
172
184
  ## Troubleshooting
173
185
 
186
+ ### `400 Bad Request` from gateway on `openclaw_chat`
187
+
188
+ Gateway versions 2026.3.24+ require `model: "openclaw"` (or `"openclaw/<agentId>"`). The MCP bridge defaults to `"openclaw"` since v1.3.0. If you're using an older bridge version, upgrade or set `OPENCLAW_MODEL=openclaw`. If you need custom model routing, set `OPENCLAW_MODEL` to the value your gateway expects.
189
+
190
+ To diagnose, enable debug logging (`DEBUG=true`) which logs the outgoing request body and gateway error responses.
191
+
174
192
  ### `405 Method Not Allowed` from gateway
175
193
 
176
194
  The OpenClaw gateway's HTTP chat completions endpoint is disabled by default. Enable it in `openclaw.json` — see [Gateway Prerequisites](#openclaw-gateway-prerequisites) above.
package/docs/index.html CHANGED
@@ -129,6 +129,7 @@ pre, code { font-family: 'JetBrains Mono', monospace; }
129
129
  display: none; gap: 2rem; font-size: 0.875rem;
130
130
  text-transform: uppercase; letter-spacing: 0.05em; font-weight: 500; color: var(--text);
131
131
  }
132
+ .hamburger { display: none; }
132
133
  .nav-links a:hover { color: var(--accent); transition: color 0.2s; }
133
134
  .nav-gh { display: flex; align-items: center; gap: 0; font-size: 0.75rem; font-weight: 700; }
134
135
  .nav-gh-btn {
@@ -288,8 +289,106 @@ pre, code { font-family: 'JetBrains Mono', monospace; }
288
289
  text-align: center; font-size: 10px; color: rgba(160,160,160,0.5);
289
290
  }
290
291
 
291
- /* === Desktop (640px+) === */
292
- @media (min-width: 640px) {
292
+ /* === Mobile Hamburger Menu === */
293
+ .hamburger {
294
+ display: flex; flex-direction: column; gap: 5px; padding: 0.5rem;
295
+ cursor: pointer; z-index: 60; background: none; border: none;
296
+ }
297
+ .hamburger span {
298
+ display: block; width: 22px; height: 2px; background: #fff;
299
+ transition: transform 0.3s, opacity 0.3s;
300
+ }
301
+ .hamburger.active span:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); }
302
+ .hamburger.active span:nth-child(2) { opacity: 0; }
303
+ .hamburger.active span:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); }
304
+
305
+ .mobile-menu {
306
+ display: none; position: fixed; inset: 0; z-index: 55;
307
+ background: rgba(8,8,8,0.97); backdrop-filter: blur(16px);
308
+ -webkit-backdrop-filter: blur(16px);
309
+ flex-direction: column; align-items: center; justify-content: center; gap: 2rem;
310
+ font-size: 1.25rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700;
311
+ }
312
+ .mobile-menu.active { display: flex; }
313
+ .mobile-menu a { color: var(--text); transition: color 0.2s; padding: 0.5rem 1rem; }
314
+ .mobile-menu a:hover, .mobile-menu a:active { color: var(--accent); }
315
+
316
+ /* === Mobile-first fixes === */
317
+ @media (max-width: 639px) {
318
+ .hamburger { display: flex; }
319
+ .nav-gh { display: none; }
320
+
321
+ .hero { padding: 7rem 0 3rem; }
322
+ .hero h1 { font-size: 2.25rem; }
323
+ .hero-sub { font-size: 1rem; margin-top: 1.25rem; }
324
+ .hero-buttons { margin-top: 2rem; gap: 0.75rem; }
325
+ .btn-primary, .btn-secondary { padding: 0.875rem 1.5rem; font-size: 1rem; }
326
+ .stats-bar { margin-top: 2.5rem; gap: 1rem; font-size: 0.75rem; }
327
+
328
+ .how-section, .features-section, .tools-section, .quickstart-section, .cta-section {
329
+ padding: 4rem 0;
330
+ }
331
+ .how-section h2, .features-section h2, .tools-section h2,
332
+ .quickstart-section h2, .cta-section h2 {
333
+ font-size: 1.75rem; margin-bottom: 2rem;
334
+ }
335
+
336
+ .how-step { padding: 1.5rem; }
337
+ .how-step-num { font-size: 1.5rem; }
338
+ .how-step h3 { font-size: 1.1rem; }
339
+
340
+ .features-grid { gap: 1rem; }
341
+ .feature-card { padding: 1.5rem; }
342
+ .feature-card h3 { font-size: 1.1rem; }
343
+
344
+ .tools-table { font-size: 0.75rem; }
345
+ .tools-table th, .tools-table td { padding: 0.75rem 0.5rem; }
346
+ .tools-col-header h3 { font-size: 1.25rem; }
347
+
348
+ .tab-btn { padding: 0.75rem 1rem; font-size: 0.65rem; }
349
+ .tab-content { padding: 1rem; }
350
+ .tab-content pre code { font-size: 0.7rem; word-break: break-all; white-space: pre-wrap; }
351
+
352
+ .cta-btn-primary, .cta-btn-secondary {
353
+ padding: 1rem 2rem; font-size: 1rem; width: 100%; text-align: center;
354
+ }
355
+
356
+ .footer { padding: 2.5rem 0; }
357
+ .footer-links { gap: 1.25rem; }
358
+ .footer-bottom { margin-top: 2rem; }
359
+
360
+ .plugin-cards { gap: 0.75rem !important; }
361
+
362
+ /* Prevent horizontal overflow */
363
+ .terminal-body { font-size: 0.75rem; padding: 1rem; }
364
+ .container { padding: 0 1rem; }
365
+ }
366
+
367
+ /* === Tablet (640px - 1023px) === */
368
+ @media (min-width: 640px) and (max-width: 1023px) {
369
+ .hamburger { display: flex; }
370
+ .nav-links { display: none; }
371
+ .hero h1 { font-size: 4rem; }
372
+ .hero-sub { font-size: 1.25rem; }
373
+ .hero-buttons { flex-direction: row; }
374
+ .btn-primary, .btn-secondary { width: auto; }
375
+ .how-grid { grid-template-columns: repeat(2, 1fr); }
376
+ .how-step:nth-child(2) { border-right: none; }
377
+ .features-grid { grid-template-columns: repeat(2, 1fr); }
378
+ .tools-grid { grid-template-columns: repeat(2, 1fr); }
379
+ .cta-section h2 { font-size: 2.5rem; }
380
+ .cta-buttons { flex-direction: row; }
381
+ .footer-inner { flex-direction: row; justify-content: space-between; align-items: center; }
382
+ .plugin-cards { grid-template-columns: repeat(2, 1fr) !important; }
383
+
384
+ .how-section, .features-section, .tools-section, .quickstart-section, .cta-section {
385
+ padding: 5rem 0;
386
+ }
387
+ }
388
+
389
+ /* === Desktop (1024px+) === */
390
+ @media (min-width: 1024px) {
391
+ .hamburger { display: none; }
293
392
  .nav-links { display: flex; }
294
393
  .hero h1 { font-size: 6rem; }
295
394
  .hero-sub { font-size: 1.5rem; }
@@ -322,6 +421,9 @@ pre, code { font-family: 'JetBrains Mono', monospace; }
322
421
  <a href="#quickstart">Quick start</a>
323
422
  <a href="#plugin">Plugin</a>
324
423
  </div>
424
+ <button class="hamburger" aria-label="Menu" onclick="document.querySelector('.mobile-menu').classList.toggle('active');this.classList.toggle('active')">
425
+ <span></span><span></span><span></span>
426
+ </button>
325
427
  <div class="nav-gh">
326
428
  <a class="nav-gh-btn" href="https://github.com/freema/openclaw-mcp" target="_blank" rel="noopener">
327
429
  <svg viewBox="0 0 16 16" width="16" height="16" fill="white"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
@@ -339,6 +441,16 @@ pre, code { font-family: 'JetBrains Mono', monospace; }
339
441
  </div>
340
442
  </nav>
341
443
 
444
+ <!-- Mobile Menu Overlay -->
445
+ <div class="mobile-menu" id="mobile-menu">
446
+ <a href="#how" onclick="closeMobileMenu()">How it works</a>
447
+ <a href="#features" onclick="closeMobileMenu()">Features</a>
448
+ <a href="#tools" onclick="closeMobileMenu()">Tools</a>
449
+ <a href="#quickstart" onclick="closeMobileMenu()">Quick start</a>
450
+ <a href="#plugin" onclick="closeMobileMenu()">Plugin</a>
451
+ <a href="https://github.com/freema/openclaw-mcp" target="_blank" rel="noopener" style="color:var(--accent)">GitHub</a>
452
+ </div>
453
+
342
454
  <!-- Hero -->
343
455
  <header class="hero">
344
456
  <div class="container">
@@ -610,6 +722,14 @@ npx openclaw-mcp --transport sse --port 3000</code></pre>
610
722
  </div>
611
723
  </footer>
612
724
 
725
+ <!-- Mobile Menu Script -->
726
+ <script>
727
+ function closeMobileMenu() {
728
+ document.querySelector('.mobile-menu').classList.remove('active');
729
+ document.querySelector('.hamburger').classList.remove('active');
730
+ }
731
+ </script>
732
+
613
733
  <!-- Tab Switching Script -->
614
734
  <script>
615
735
  (function() {
@@ -91,9 +91,11 @@ openclaw-mcp --help
91
91
  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
+ --model, -m Model name for chat [default: "openclaw"]
94
95
  --transport, -t Transport mode [choices: "stdio", "sse"] [default: "stdio"]
95
96
  --port, -p Port for SSE server [default: 3000]
96
97
  --host Host for SSE server [default: "0.0.0.0"]
98
+ --debug Enable debug logging [default: false]
97
99
  --auth Enable OAuth [default: false]
98
100
  --client-id MCP OAuth client ID [env: MCP_CLIENT_ID]
99
101
  --client-secret MCP OAuth client secret [env: MCP_CLIENT_SECRET]
package/docs/logging.md CHANGED
@@ -36,13 +36,24 @@ The MCP server logs operational events to **stderr** using the `[openclaw-mcp]`
36
36
  - Invalid client configuration (missing secrets, bad client ID format)
37
37
  - Session errors
38
38
 
39
- ### What Is NOT Logged
39
+ ### What Is NOT Logged (Info/Error levels)
40
40
 
41
41
  - **Message content** — user messages and OpenClaw responses are never logged
42
42
  - **Authentication tokens** — Bearer tokens, client secrets, gateway tokens
43
43
  - **Request/response bodies** — only error messages, not full payloads
44
44
  - **User-identifiable information** — no IPs, user agents, or personal data
45
45
 
46
+ ### Debug Level (`DEBUG=true`)
47
+
48
+ > **Warning:** Debug mode is a diagnostic tool. It logs request and response payloads which may contain user message content. Do not enable in production under normal operation — use it only for active troubleshooting, then disable it.
49
+
50
+ When debug logging is enabled, the following **are** logged for troubleshooting:
51
+
52
+ - **Request bodies** — outgoing payloads sent to the gateway (truncated to 4096 chars)
53
+ - **Error response bodies** — full error responses from the gateway (truncated to 4096 chars)
54
+
55
+ Credentials are still redacted by the sanitization layer. Headers (including Authorization) are never logged, even in debug mode.
56
+
46
57
  ## Sensitive Data Redaction
47
58
 
48
59
  The logger automatically redacts patterns that look like credentials:
@@ -99,4 +110,4 @@ DEBUG=true # Explicit debug flag
99
110
  NODE_ENV=development # Development mode
100
111
  ```
101
112
 
102
- Debug logs include additional operational detail but still never log message content or credentials.
113
+ Debug logs include request/response bodies for troubleshooting (truncated to 4096 chars). Credentials are still redacted, and headers (including Authorization) are never logged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Model Context Protocol (MCP) server for OpenClaw AI assistant integration",
5
5
  "author": "Tomáš Grasl <https://www.tomasgrasl.cz/>",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "dev": "tsx watch src/index.ts",
15
15
  "build": "tsup",
16
16
  "start": "node dist/index.js",
17
- "clean": "rm -rf dist",
17
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
18
18
  "typecheck": "tsc --noEmit",
19
19
  "lint": "eslint src --ext .ts",
20
20
  "lint:fix": "eslint src --ext .ts --fix",