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/README.md +78 -0
- package/dist/index.js +358 -50
- package/docs/CNAME +1 -0
- package/docs/assets/og-image.png +0 -0
- package/docs/configuration.md +113 -21
- package/docs/deployment.md +18 -0
- package/docs/index.html +773 -0
- package/docs/installation.md +2 -0
- package/docs/logging.md +13 -2
- package/package.json +2 -2
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 =
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
379
|
-
|
|
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
|
|
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
|
|
611
|
-
async function processTask(task,
|
|
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
|
|
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 (!
|
|
889
|
+
if (!processorRegistry) return;
|
|
624
890
|
while (processorRunning) {
|
|
625
891
|
const task = taskManager.getNextPending();
|
|
626
892
|
if (task) {
|
|
627
|
-
await processTask(task,
|
|
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(
|
|
899
|
+
function startTaskProcessor(registry2) {
|
|
634
900
|
if (processorRunning) return;
|
|
635
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
1112
|
+
const { registry: registry2 } = deps2;
|
|
820
1113
|
const toolHandlers = /* @__PURE__ */ new Map([
|
|
821
|
-
["openclaw_chat", (input) => handleOpenclawChat(
|
|
822
|
-
["openclaw_status", (input) => handleOpenclawStatus(
|
|
823
|
-
["openclaw_chat_async", (input) => handleOpenclawChatAsync(
|
|
824
|
-
["openclaw_task_status", (input) => handleOpenclawTaskStatus(
|
|
825
|
-
["openclaw_task_list", (input) => handleOpenclawTaskList(
|
|
826
|
-
["openclaw_task_cancel", (input) => handleOpenclawTaskCancel(
|
|
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(
|
|
1239
|
+
async authorize(client, params, res) {
|
|
945
1240
|
const code = randomUUID();
|
|
946
|
-
this.codes.set(code, { client
|
|
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(
|
|
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 !==
|
|
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:
|
|
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:
|
|
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(
|
|
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 !==
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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,
|