openclaw-mcp 1.2.1 → 1.3.1

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/dist/index.js CHANGED
@@ -4,15 +4,14 @@
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
6
  // src/config/constants.ts
7
- import { createRequire } from "module";
8
- var require2 = createRequire(import.meta.url);
9
- var pkg = require2("../../package.json");
10
7
  var SERVER_NAME = "openclaw-mcp";
11
- var SERVER_VERSION = pkg.version;
8
+ var SERVER_VERSION = "1.3.1";
12
9
  var DEFAULT_OPENCLAW_URL = "http://127.0.0.1:18789";
10
+ var DEFAULT_MODEL = "openclaw";
13
11
  var SERVER_ICON_SVG_BASE64 = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgZmlsbD0ibm9uZSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzFhMWEyZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzE2MjEzZSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjbGF3IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZmYzMzMzIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjY2MwMDAwIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIyNCIgZmlsbD0idXJsKCNiZykiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NCA2NCkiIHN0cm9rZT0idXJsKCNjbGF3KSIgc3Ryb2tlLXdpZHRoPSI3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiPjxwYXRoIGQ9Ik0tMjggLTM4YzAgMCAtMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0tMTIgLTQwYzAgMCAtNiAyMiA0IDM0Ii8+PHBhdGggZD0iTTI4IC0zOGMwIDAgMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0xMiAtNDBjMCAwIDYgMjIgLTQgMzQiLz48Y2lyY2xlIGN4PSIwIiBjeT0iMTAiIHI9IjIwIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNLTEwIDR2LTQiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Ik0xMCA0di00IiBzdHJva2Utd2lkdGg9IjQiLz48cGF0aCBkPSJNLTggMjBjNCA2IDEyIDYgMTYgMCIgc3Ryb2tlLXdpZHRoPSIzIi8+PC9nPjwvc3ZnPg==";
14
12
 
15
13
  // src/utils/logger.ts
14
+ var debugEnabled = false;
16
15
  var SENSITIVE_PATTERNS = [
17
16
  /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
18
17
  /api[_-]?key["\s:=]+[A-Za-z0-9\-._~+/]{8,}/gi,
@@ -30,6 +29,19 @@ function sanitizeLogMessage(message) {
30
29
  function log(message) {
31
30
  console.error(`[openclaw-mcp] ${sanitizeLogMessage(message)}`);
32
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
+ }
33
45
  function logError(message, error) {
34
46
  console.error(`[openclaw-mcp] ERROR: ${sanitizeLogMessage(message)}`);
35
47
  if (error) {
@@ -54,6 +66,11 @@ function parseArguments(version) {
54
66
  type: "string",
55
67
  description: "Bearer token for OpenClaw gateway authentication",
56
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
57
74
  }).option("transport", {
58
75
  alias: "t",
59
76
  type: "string",
@@ -73,6 +90,10 @@ function parseArguments(version) {
73
90
  type: "number",
74
91
  description: "Request timeout in milliseconds",
75
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"
76
97
  }).option("auth", {
77
98
  type: "boolean",
78
99
  description: "Enable OAuth authentication (SSE mode)",
@@ -94,18 +115,60 @@ function parseArguments(version) {
94
115
  description: "Allowed OAuth redirect URIs (comma-separated)",
95
116
  default: process.env.MCP_REDIRECT_URIS || void 0
96
117
  }).help().parseSync();
118
+ let instances;
119
+ const instancesEnv = process.env.OPENCLAW_INSTANCES;
120
+ if (instancesEnv) {
121
+ try {
122
+ const parsed = JSON.parse(instancesEnv);
123
+ if (!Array.isArray(parsed) || parsed.length === 0) {
124
+ throw new Error("OPENCLAW_INSTANCES must be a non-empty JSON array");
125
+ }
126
+ for (const item of parsed) {
127
+ if (!item || typeof item.name !== "string" || !item.name.trim()) {
128
+ throw new Error(
129
+ 'Each instance in OPENCLAW_INSTANCES must have a non-empty string "name"'
130
+ );
131
+ }
132
+ if (typeof item.url !== "string" || !item.url.trim()) {
133
+ throw new Error(`Instance "${item.name}": must have a non-empty string "url"`);
134
+ }
135
+ }
136
+ instances = parsed.map((cfg) => ({
137
+ ...cfg,
138
+ timeout: cfg.timeout ?? argv.timeout
139
+ }));
140
+ } catch (error) {
141
+ if (error instanceof SyntaxError) {
142
+ throw new Error(`OPENCLAW_INSTANCES contains invalid JSON: ${error.message}`);
143
+ }
144
+ throw error;
145
+ }
146
+ } else {
147
+ instances = [
148
+ {
149
+ name: "default",
150
+ url: argv["openclaw-url"],
151
+ token: argv["gateway-token"],
152
+ timeout: argv.timeout,
153
+ default: true
154
+ }
155
+ ];
156
+ }
97
157
  return {
98
158
  openclawUrl: argv["openclaw-url"],
99
159
  gatewayToken: argv["gateway-token"],
160
+ model: argv.model,
100
161
  transport: argv.transport,
101
162
  port: argv.port,
102
163
  host: argv.host,
103
164
  timeout: argv.timeout,
165
+ debug: argv.debug,
104
166
  authEnabled: argv.auth,
105
167
  clientId: argv["client-id"],
106
168
  clientSecret: argv["client-secret"],
107
169
  issuerUrl: argv["issuer-url"],
108
- redirectUris: argv["redirect-uris"] ? argv["redirect-uris"].split(",").map((s) => s.trim()).filter(Boolean) : void 0
170
+ redirectUris: argv["redirect-uris"] ? argv["redirect-uris"].split(",").map((s) => s.trim()).filter(Boolean) : void 0,
171
+ instances
109
172
  };
110
173
  }
111
174
 
@@ -134,14 +197,17 @@ var OpenClawApiError = class extends OpenClawError {
134
197
  // src/openclaw/client.ts
135
198
  var DEFAULT_TIMEOUT_MS = 12e4;
136
199
  var MAX_RESPONSE_SIZE_BYTES = 10 * 1024 * 1024;
200
+ var MAX_DEBUG_BODY_LENGTH = 4096;
137
201
  var OpenClawClient = class {
138
202
  baseUrl;
139
203
  gatewayToken;
140
204
  timeoutMs;
141
- constructor(baseUrl, gatewayToken, timeoutMs = DEFAULT_TIMEOUT_MS) {
205
+ model;
206
+ constructor(baseUrl, gatewayToken, timeoutMs = DEFAULT_TIMEOUT_MS, model = "openclaw") {
142
207
  this.baseUrl = baseUrl.replace(/\/$/, "");
143
208
  this.gatewayToken = gatewayToken;
144
209
  this.timeoutMs = timeoutMs;
210
+ this.model = model;
145
211
  }
146
212
  buildHeaders() {
147
213
  const headers = {
@@ -152,8 +218,16 @@ var OpenClawClient = class {
152
218
  }
153
219
  return headers;
154
220
  }
221
+ truncateForLog(value) {
222
+ if (value.length <= MAX_DEBUG_BODY_LENGTH) return value;
223
+ return value.slice(0, MAX_DEBUG_BODY_LENGTH) + `... (truncated, ${value.length} chars total)`;
224
+ }
155
225
  async request(path, options = {}) {
156
226
  const url = `${this.baseUrl}${path}`;
227
+ logDebug(() => `Request: ${options.method ?? "GET"} ${url}`);
228
+ if (options.body) {
229
+ logDebug(() => `Request body: ${this.truncateForLog(options.body)}`);
230
+ }
157
231
  const controller = new AbortController();
158
232
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
159
233
  try {
@@ -166,11 +240,23 @@ var OpenClawClient = class {
166
240
  }
167
241
  });
168
242
  if (!response.ok) {
243
+ if (isDebugEnabled()) {
244
+ const contentLength2 = response.headers.get("content-length");
245
+ if (!contentLength2 || parseInt(contentLength2, 10) <= MAX_RESPONSE_SIZE_BYTES) {
246
+ const errorBody = await response.text();
247
+ if (errorBody.length <= MAX_RESPONSE_SIZE_BYTES) {
248
+ logDebug(
249
+ () => `Response error (${response.status}): ${this.truncateForLog(errorBody)}`
250
+ );
251
+ }
252
+ }
253
+ }
169
254
  throw new OpenClawApiError(
170
255
  `API request failed: ${response.status} ${response.statusText}`,
171
256
  response.status
172
257
  );
173
258
  }
259
+ logDebug(() => `Response: ${response.status} ${response.statusText}`);
174
260
  const contentLength = response.headers.get("content-length");
175
261
  if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE_BYTES) {
176
262
  throw new OpenClawApiError("Response exceeds maximum allowed size (10MB)", 413);
@@ -239,7 +325,7 @@ var OpenClawClient = class {
239
325
  */
240
326
  async chat(message, sessionId) {
241
327
  const body = {
242
- model: "claude-opus-4-5",
328
+ model: this.model,
243
329
  messages: [{ role: "user", content: message }],
244
330
  max_tokens: 4096
245
331
  };
@@ -264,6 +350,120 @@ var OpenClawClient = class {
264
350
  }
265
351
  };
266
352
 
353
+ // src/openclaw/registry.ts
354
+ var INSTANCE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
355
+ var InstanceRegistry = class {
356
+ instances = /* @__PURE__ */ new Map();
357
+ defaultName;
358
+ constructor(configs, model) {
359
+ if (configs.length === 0) {
360
+ throw new Error("At least one OpenClaw instance must be configured");
361
+ }
362
+ const names = /* @__PURE__ */ new Set();
363
+ let explicitDefault;
364
+ for (const config of configs) {
365
+ if (!INSTANCE_NAME_RE.test(config.name)) {
366
+ throw new Error(
367
+ `Invalid instance name "${config.name}": must be 1-64 chars, alphanumeric/dashes/underscores, start with alphanumeric`
368
+ );
369
+ }
370
+ try {
371
+ const parsed = new URL(config.url);
372
+ if (!["http:", "https:"].includes(parsed.protocol)) {
373
+ throw new Error(
374
+ `Instance "${config.name}": URL must use http or https (got ${parsed.protocol})`
375
+ );
376
+ }
377
+ } catch (error) {
378
+ if (error instanceof TypeError) {
379
+ throw new Error(`Instance "${config.name}": invalid URL "${config.url}"`);
380
+ }
381
+ throw error;
382
+ }
383
+ if (names.has(config.name)) {
384
+ throw new Error(`Duplicate instance name: "${config.name}"`);
385
+ }
386
+ names.add(config.name);
387
+ if (config.default) {
388
+ if (explicitDefault) {
389
+ throw new Error(
390
+ `Multiple default instances: "${explicitDefault}" and "${config.name}". Only one default is allowed.`
391
+ );
392
+ }
393
+ explicitDefault = config.name;
394
+ }
395
+ const client = new OpenClawClient(config.url, config.token, config.timeout, model);
396
+ this.instances.set(config.name, { config, client });
397
+ }
398
+ this.defaultName = explicitDefault ?? configs[0].name;
399
+ }
400
+ /**
401
+ * Get client by instance name. Returns undefined if not found.
402
+ */
403
+ get(name) {
404
+ return this.instances.get(name)?.client;
405
+ }
406
+ /**
407
+ * Get the default client.
408
+ */
409
+ getDefault() {
410
+ const entry = this.instances.get(this.defaultName);
411
+ if (!entry) {
412
+ throw new Error(`Default instance "${this.defaultName}" not found`);
413
+ }
414
+ return entry.client;
415
+ }
416
+ /**
417
+ * Get the default instance name.
418
+ */
419
+ getDefaultName() {
420
+ return this.defaultName;
421
+ }
422
+ /**
423
+ * Resolve an optional instance name to a concrete client.
424
+ * Falls back to default when name is undefined.
425
+ */
426
+ resolve(name) {
427
+ if (!name) {
428
+ return { name: this.defaultName, client: this.getDefault() };
429
+ }
430
+ const client = this.get(name);
431
+ if (!client) {
432
+ const available = this.listNames().join(", ");
433
+ throw new Error(`Unknown instance "${name}". Available: ${available}`);
434
+ }
435
+ return { name, client };
436
+ }
437
+ /**
438
+ * List instance names.
439
+ */
440
+ listNames() {
441
+ return Array.from(this.instances.keys());
442
+ }
443
+ /**
444
+ * List instances with safe metadata (never exposes tokens).
445
+ */
446
+ list() {
447
+ return Array.from(this.instances.entries()).map(([name, { config }]) => ({
448
+ name,
449
+ url: config.url,
450
+ isDefault: name === this.defaultName
451
+ }));
452
+ }
453
+ /**
454
+ * Number of registered instances.
455
+ */
456
+ get size() {
457
+ return this.instances.size;
458
+ }
459
+ /**
460
+ * Check if this is a single-instance (backward-compat) setup.
461
+ */
462
+ get isSingleInstance() {
463
+ return this.instances.size === 1;
464
+ }
465
+ };
466
+
267
467
  // src/server/tools-registration.ts
268
468
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
269
469
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -332,12 +532,16 @@ var openclawChatTool = {
332
532
  session_id: {
333
533
  type: "string",
334
534
  description: "Optional session ID for conversation context"
535
+ },
536
+ instance: {
537
+ type: "string",
538
+ description: "Target OpenClaw instance name. Use openclaw_instances to list available instances. Defaults to the default instance."
335
539
  }
336
540
  },
337
541
  required: ["message"]
338
542
  }
339
543
  };
340
- async function handleOpenclawChat(client2, input) {
544
+ async function handleOpenclawChat(registry2, input) {
341
545
  if (!validateInputIsObject(input)) {
342
546
  return errorResponse("Invalid input: expected an object");
343
547
  }
@@ -353,8 +557,17 @@ async function handleOpenclawChat(client2, input) {
353
557
  }
354
558
  sessionId = sidResult.value;
355
559
  }
560
+ let instanceName;
561
+ if (input.instance !== void 0) {
562
+ const instResult = validateId(input.instance, "instance");
563
+ if (instResult.valid === false) {
564
+ return errorResponse(instResult.error);
565
+ }
566
+ instanceName = instResult.value;
567
+ }
356
568
  try {
357
- const response = await client2.chat(msgResult.value, sessionId);
569
+ const { client } = registry2.resolve(instanceName);
570
+ const response = await client.chat(msgResult.value, sessionId);
358
571
  return successResponse(response.response);
359
572
  } catch (error) {
360
573
  return errorResponse(error instanceof Error ? error.message : "Failed to chat with OpenClaw");
@@ -367,16 +580,33 @@ var openclawStatusTool = {
367
580
  description: "Get OpenClaw gateway status and health information",
368
581
  inputSchema: {
369
582
  type: "object",
370
- properties: {}
583
+ properties: {
584
+ instance: {
585
+ type: "string",
586
+ description: "Target OpenClaw instance name. Defaults to the default instance."
587
+ }
588
+ }
371
589
  }
372
590
  };
373
- async function handleOpenclawStatus(client2, input) {
591
+ async function handleOpenclawStatus(registry2, input) {
374
592
  if (!validateInputIsObject(input)) {
375
593
  return errorResponse("Invalid input: expected an object");
376
594
  }
595
+ let instanceName;
596
+ if (input.instance !== void 0) {
597
+ const instResult = validateId(input.instance, "instance");
598
+ if (instResult.valid === false) {
599
+ return errorResponse(instResult.error);
600
+ }
601
+ instanceName = instResult.value;
602
+ }
377
603
  try {
378
- const response = await client2.health();
379
- return jsonResponse(response);
604
+ const resolved = registry2.resolve(instanceName);
605
+ const response = await resolved.client.health();
606
+ return jsonResponse({
607
+ ...response,
608
+ instance: resolved.name
609
+ });
380
610
  } catch (error) {
381
611
  return errorResponse(
382
612
  error instanceof Error ? error.message : "Failed to get status from OpenClaw"
@@ -384,6 +614,22 @@ async function handleOpenclawStatus(client2, input) {
384
614
  }
385
615
  }
386
616
 
617
+ // src/mcp/tools/instances.ts
618
+ var openclawInstancesTool = {
619
+ name: "openclaw_instances",
620
+ description: "List all configured OpenClaw instances. Shows instance names, URLs, and which is the default. Use instance names in other tools to target a specific OpenClaw gateway.",
621
+ inputSchema: {
622
+ type: "object",
623
+ properties: {}
624
+ }
625
+ };
626
+ async function handleOpenclawInstances(registry2, _input) {
627
+ return jsonResponse({
628
+ instances: registry2.list(),
629
+ total: registry2.size
630
+ });
631
+ }
632
+
387
633
  // src/mcp/tasks/manager.ts
388
634
  var MAX_TASKS = 1e3;
389
635
  var CLEANUP_INTERVAL_MS = 10 * 60 * 1e3;
@@ -424,6 +670,7 @@ var TaskManager = class {
424
670
  input: options.input,
425
671
  createdAt: /* @__PURE__ */ new Date(),
426
672
  sessionId: options.sessionId,
673
+ instanceId: options.instanceId,
427
674
  priority: options.priority ?? 0
428
675
  };
429
676
  this.tasks.set(id, task);
@@ -447,6 +694,9 @@ var TaskManager = class {
447
694
  if (filter?.sessionId) {
448
695
  tasks = tasks.filter((t) => t.sessionId === filter.sessionId);
449
696
  }
697
+ if (filter?.instanceId) {
698
+ tasks = tasks.filter((t) => t.instanceId === filter.instanceId);
699
+ }
450
700
  return tasks.sort((a, b) => {
451
701
  if (b.priority !== a.priority) return b.priority - a.priority;
452
702
  return a.createdAt.getTime() - b.createdAt.getTime();
@@ -554,6 +804,10 @@ var openclawChatAsyncTool = {
554
804
  priority: {
555
805
  type: "number",
556
806
  description: "Task priority (higher = processed first). Default: 0"
807
+ },
808
+ instance: {
809
+ type: "string",
810
+ description: "Target OpenClaw instance name. Defaults to the default instance."
557
811
  }
558
812
  },
559
813
  required: ["message"]
@@ -575,7 +829,7 @@ var openclawTaskStatusTool = {
575
829
  };
576
830
  var openclawTaskListTool = {
577
831
  name: "openclaw_task_list",
578
- description: "List all tasks. Optionally filter by status or session.",
832
+ description: "List all tasks. Optionally filter by status, session, or instance.",
579
833
  inputSchema: {
580
834
  type: "object",
581
835
  properties: {
@@ -587,6 +841,10 @@ var openclawTaskListTool = {
587
841
  session_id: {
588
842
  type: "string",
589
843
  description: "Filter by session ID"
844
+ },
845
+ instance: {
846
+ type: "string",
847
+ description: "Filter by instance name"
590
848
  }
591
849
  },
592
850
  required: []
@@ -607,12 +865,20 @@ var openclawTaskCancelTool = {
607
865
  }
608
866
  };
609
867
  var processorRunning = false;
610
- var processorClient = null;
611
- async function processTask(task, client2) {
868
+ var processorRegistry = null;
869
+ async function processTask(task, registry2) {
612
870
  taskManager.updateStatus(task.id, "running");
871
+ let client;
872
+ try {
873
+ client = registry2.resolve(task.instanceId).client;
874
+ } catch (error) {
875
+ const errorMsg = error instanceof Error ? error.message : "Instance not available";
876
+ taskManager.updateStatus(task.id, "failed", void 0, errorMsg);
877
+ return;
878
+ }
613
879
  try {
614
880
  const input = task.input;
615
- const response = await client2.chat(input.message, input.session_id);
881
+ const response = await client.chat(input.message, input.session_id);
616
882
  taskManager.updateStatus(task.id, "completed", response.response);
617
883
  } catch (error) {
618
884
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
@@ -620,26 +886,26 @@ async function processTask(task, client2) {
620
886
  }
621
887
  }
622
888
  async function taskProcessor() {
623
- if (!processorClient) return;
889
+ if (!processorRegistry) return;
624
890
  while (processorRunning) {
625
891
  const task = taskManager.getNextPending();
626
892
  if (task) {
627
- await processTask(task, processorClient);
893
+ await processTask(task, processorRegistry);
628
894
  } else {
629
895
  await new Promise((resolve) => setTimeout(resolve, 100));
630
896
  }
631
897
  }
632
898
  }
633
- function startTaskProcessor(client2) {
899
+ function startTaskProcessor(registry2) {
634
900
  if (processorRunning) return;
635
- processorClient = client2;
901
+ processorRegistry = registry2;
636
902
  processorRunning = true;
637
903
  taskProcessor().catch(() => {
638
904
  processorRunning = false;
639
905
  });
640
906
  log("Task processor started");
641
907
  }
642
- async function handleOpenclawChatAsync(client2, input) {
908
+ async function handleOpenclawChatAsync(registry2, input) {
643
909
  if (!validateInputIsObject(input)) {
644
910
  return errorResponse("Invalid input: expected an object");
645
911
  }
@@ -662,18 +928,35 @@ async function handleOpenclawChatAsync(client2, input) {
662
928
  }
663
929
  priority = input.priority;
664
930
  }
665
- startTaskProcessor(client2);
931
+ let instanceId;
932
+ try {
933
+ let instanceName;
934
+ if (input.instance !== void 0) {
935
+ const instResult = validateId(input.instance, "instance");
936
+ if (instResult.valid === false) {
937
+ return errorResponse(instResult.error);
938
+ }
939
+ instanceName = instResult.value;
940
+ }
941
+ const resolved = registry2.resolve(instanceName);
942
+ instanceId = resolved.name;
943
+ } catch (error) {
944
+ return errorResponse(error instanceof Error ? error.message : "Invalid instance");
945
+ }
946
+ startTaskProcessor(registry2);
666
947
  const task = taskManager.create({
667
948
  type: "chat",
668
949
  input: { message: msgResult.value, session_id: sessionId },
669
950
  sessionId,
670
- priority
951
+ priority,
952
+ instanceId
671
953
  });
672
954
  return successResponse(
673
955
  JSON.stringify(
674
956
  {
675
957
  task_id: task.id,
676
958
  status: task.status,
959
+ instance: instanceId,
677
960
  message: "Task queued. Use openclaw_task_status to check progress."
678
961
  },
679
962
  null,
@@ -681,7 +964,7 @@ async function handleOpenclawChatAsync(client2, input) {
681
964
  )
682
965
  );
683
966
  }
684
- async function handleOpenclawTaskStatus(_client, input) {
967
+ async function handleOpenclawTaskStatus(_registry, input) {
685
968
  if (!validateInputIsObject(input)) {
686
969
  return errorResponse("Invalid input: expected an object");
687
970
  }
@@ -698,6 +981,7 @@ async function handleOpenclawTaskStatus(_client, input) {
698
981
  task_id: task.id,
699
982
  type: task.type,
700
983
  status: task.status,
984
+ instance: task.instanceId,
701
985
  created_at: task.createdAt.toISOString()
702
986
  };
703
987
  if (task.startedAt) {
@@ -721,7 +1005,7 @@ var VALID_TASK_STATUSES = [
721
1005
  "failed",
722
1006
  "cancelled"
723
1007
  ];
724
- async function handleOpenclawTaskList(_client, input) {
1008
+ async function handleOpenclawTaskList(_registry, input) {
725
1009
  if (!validateInputIsObject(input)) {
726
1010
  return errorResponse("Invalid input: expected an object");
727
1011
  }
@@ -740,12 +1024,21 @@ async function handleOpenclawTaskList(_client, input) {
740
1024
  }
741
1025
  session_id = sidResult.value;
742
1026
  }
743
- const tasks = taskManager.list({ status, sessionId: session_id });
1027
+ let instanceFilter;
1028
+ if (input.instance !== void 0) {
1029
+ const instResult = validateId(input.instance, "instance");
1030
+ if (instResult.valid === false) {
1031
+ return errorResponse(instResult.error);
1032
+ }
1033
+ instanceFilter = instResult.value;
1034
+ }
1035
+ const tasks = taskManager.list({ status, sessionId: session_id, instanceId: instanceFilter });
744
1036
  const stats = taskManager.stats();
745
1037
  const taskList = tasks.map((t) => ({
746
1038
  task_id: t.id,
747
1039
  type: t.type,
748
1040
  status: t.status,
1041
+ instance: t.instanceId,
749
1042
  priority: t.priority,
750
1043
  created_at: t.createdAt.toISOString(),
751
1044
  has_result: t.status === "completed" && !!t.result
@@ -761,7 +1054,7 @@ async function handleOpenclawTaskList(_client, input) {
761
1054
  )
762
1055
  );
763
1056
  }
764
- async function handleOpenclawTaskCancel(_client, input) {
1057
+ async function handleOpenclawTaskCancel(_registry, input) {
765
1058
  if (!validateInputIsObject(input)) {
766
1059
  return errorResponse("Invalid input: expected an object");
767
1060
  }
@@ -816,14 +1109,15 @@ function createMcpServer(deps2) {
816
1109
  return server;
817
1110
  }
818
1111
  function registerTools(server, deps2) {
819
- const { client: client2 } = deps2;
1112
+ const { registry: registry2 } = deps2;
820
1113
  const toolHandlers = /* @__PURE__ */ new Map([
821
- ["openclaw_chat", (input) => handleOpenclawChat(client2, input)],
822
- ["openclaw_status", (input) => handleOpenclawStatus(client2, input)],
823
- ["openclaw_chat_async", (input) => handleOpenclawChatAsync(client2, input)],
824
- ["openclaw_task_status", (input) => handleOpenclawTaskStatus(client2, input)],
825
- ["openclaw_task_list", (input) => handleOpenclawTaskList(client2, input)],
826
- ["openclaw_task_cancel", (input) => handleOpenclawTaskCancel(client2, input)]
1114
+ ["openclaw_chat", (input) => handleOpenclawChat(registry2, input)],
1115
+ ["openclaw_status", (input) => handleOpenclawStatus(registry2, input)],
1116
+ ["openclaw_chat_async", (input) => handleOpenclawChatAsync(registry2, input)],
1117
+ ["openclaw_task_status", (input) => handleOpenclawTaskStatus(registry2, input)],
1118
+ ["openclaw_task_list", (input) => handleOpenclawTaskList(registry2, input)],
1119
+ ["openclaw_task_cancel", (input) => handleOpenclawTaskCancel(registry2, input)],
1120
+ ["openclaw_instances", (input) => handleOpenclawInstances(registry2, input)]
827
1121
  ]);
828
1122
  const allTools = [
829
1123
  openclawChatTool,
@@ -831,7 +1125,8 @@ function registerTools(server, deps2) {
831
1125
  openclawChatAsyncTool,
832
1126
  openclawTaskStatusTool,
833
1127
  openclawTaskListTool,
834
- openclawTaskCancelTool
1128
+ openclawTaskCancelTool,
1129
+ openclawInstancesTool
835
1130
  ];
836
1131
  server.setRequestHandler(ListToolsRequestSchema, async () => {
837
1132
  return { tools: allTools };
@@ -941,9 +1236,9 @@ var OpenClawAuthProvider = class {
941
1236
  /**
942
1237
  * Auto-approve: generate auth code and redirect immediately.
943
1238
  */
944
- async authorize(client2, params, res) {
1239
+ async authorize(client, params, res) {
945
1240
  const code = randomUUID();
946
- this.codes.set(code, { client: client2, params, createdAt: Date.now() });
1241
+ this.codes.set(code, { client, params, createdAt: Date.now() });
947
1242
  const searchParams = new URLSearchParams({ code });
948
1243
  if (params.state !== void 0) {
949
1244
  searchParams.set("state", params.state);
@@ -960,13 +1255,13 @@ var OpenClawAuthProvider = class {
960
1255
  }
961
1256
  return codeData.params.codeChallenge;
962
1257
  }
963
- async exchangeAuthorizationCode(client2, authorizationCode, _codeVerifier, _redirectUri, resource) {
1258
+ async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, _redirectUri, resource) {
964
1259
  const codeData = this.codes.get(authorizationCode);
965
1260
  if (!codeData || Date.now() - codeData.createdAt > AUTH_CODE_TTL_MS) {
966
1261
  if (codeData) this.codes.delete(authorizationCode);
967
1262
  throw new InvalidRequestError("Invalid authorization code");
968
1263
  }
969
- if (codeData.client.client_id !== client2.client_id) {
1264
+ if (codeData.client.client_id !== client.client_id) {
970
1265
  throw new InvalidRequestError("Authorization code was not issued to this client");
971
1266
  }
972
1267
  this.codes.delete(authorizationCode);
@@ -975,13 +1270,13 @@ var OpenClawAuthProvider = class {
975
1270
  const scopes = codeData.params.scopes || [];
976
1271
  this.tokens.set(accessToken, {
977
1272
  token: accessToken,
978
- clientId: client2.client_id,
1273
+ clientId: client.client_id,
979
1274
  scopes,
980
1275
  expiresAt: Date.now() + TOKEN_TTL_MS,
981
1276
  resource: resource || codeData.params.resource
982
1277
  });
983
1278
  this.refreshTokens.set(refreshToken, {
984
- clientId: client2.client_id,
1279
+ clientId: client.client_id,
985
1280
  scopes,
986
1281
  expiresAt: Date.now() + REFRESH_TOKEN_TTL_MS,
987
1282
  resource: resource || codeData.params.resource
@@ -994,13 +1289,13 @@ var OpenClawAuthProvider = class {
994
1289
  scope: scopes.join(" ")
995
1290
  };
996
1291
  }
997
- async exchangeRefreshToken(client2, refreshToken, scopes, resource) {
1292
+ async exchangeRefreshToken(client, refreshToken, scopes, resource) {
998
1293
  const data = this.refreshTokens.get(refreshToken);
999
1294
  if (!data || data.expiresAt < Date.now()) {
1000
1295
  if (data) this.refreshTokens.delete(refreshToken);
1001
1296
  throw new InvalidRequestError("Invalid refresh token");
1002
1297
  }
1003
- if (data.clientId !== client2.client_id) {
1298
+ if (data.clientId !== client.client_id) {
1004
1299
  throw new InvalidRequestError("Refresh token was not issued to this client");
1005
1300
  }
1006
1301
  this.refreshTokens.delete(refreshToken);
@@ -1009,13 +1304,13 @@ var OpenClawAuthProvider = class {
1009
1304
  const tokenScopes = scopes || data.scopes;
1010
1305
  this.tokens.set(accessToken, {
1011
1306
  token: accessToken,
1012
- clientId: client2.client_id,
1307
+ clientId: client.client_id,
1013
1308
  scopes: tokenScopes,
1014
1309
  expiresAt: Date.now() + TOKEN_TTL_MS,
1015
1310
  resource: resource || data.resource
1016
1311
  });
1017
1312
  this.refreshTokens.set(newRefreshToken, {
1018
- clientId: client2.client_id,
1313
+ clientId: client.client_id,
1019
1314
  scopes: tokenScopes,
1020
1315
  expiresAt: Date.now() + REFRESH_TOKEN_TTL_MS,
1021
1316
  resource: resource || data.resource
@@ -1284,18 +1579,31 @@ async function createSSEServer(config, deps2) {
1284
1579
 
1285
1580
  // src/index.ts
1286
1581
  var args = parseArguments(SERVER_VERSION);
1287
- var client = new OpenClawClient(args.openclawUrl, args.gatewayToken, args.timeout);
1582
+ setDebugEnabled(args.debug);
1583
+ var trimmedModel = args.model.trim();
1584
+ if (!trimmedModel) {
1585
+ logError('OPENCLAW_MODEL / --model must be a non-empty string. Default is "openclaw".');
1586
+ process.exit(1);
1587
+ }
1588
+ args.model = trimmedModel;
1589
+ var registry = new InstanceRegistry(args.instances, args.model);
1288
1590
  var deps = {
1289
- client,
1591
+ registry,
1290
1592
  serverName: SERVER_NAME,
1291
1593
  serverVersion: SERVER_VERSION
1292
1594
  };
1293
1595
  async function main() {
1294
1596
  log(`Starting ${SERVER_NAME} v${SERVER_VERSION}`);
1295
- log(`OpenClaw URL: ${args.openclawUrl}`);
1597
+ log(`Model: ${args.model}`);
1296
1598
  log(`Transport: ${args.transport}`);
1297
- log(`Gateway token: ${args.gatewayToken ? "configured" : "not set"}`);
1298
1599
  log(`Request timeout: ${args.timeout}ms`);
1600
+ if (args.debug) {
1601
+ log("Debug logging: enabled");
1602
+ }
1603
+ for (const instance of registry.list()) {
1604
+ const defaultLabel = instance.isDefault ? " (default)" : "";
1605
+ log(`Instance "${instance.name}": ${instance.url}${defaultLabel}`);
1606
+ }
1299
1607
  if (args.transport === "sse") {
1300
1608
  const sseConfig = {
1301
1609
  port: args.port,