openclaw-mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1326 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/config/constants.ts
7
+ var SERVER_NAME = "openclaw-mcp";
8
+ var SERVER_VERSION = "1.0.0";
9
+ var DEFAULT_OPENCLAW_URL = "http://127.0.0.1:18789";
10
+ var SERVER_ICON_SVG_BASE64 = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgZmlsbD0ibm9uZSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzFhMWEyZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzE2MjEzZSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjbGF3IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZmYzMzMzIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjY2MwMDAwIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIyNCIgZmlsbD0idXJsKCNiZykiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NCA2NCkiIHN0cm9rZT0idXJsKCNjbGF3KSIgc3Ryb2tlLXdpZHRoPSI3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiPjxwYXRoIGQ9Ik0tMjggLTM4YzAgMCAtMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0tMTIgLTQwYzAgMCAtNiAyMiA0IDM0Ii8+PHBhdGggZD0iTTI4IC0zOGMwIDAgMTAgMjAgMCAzMiIvPjxwYXRoIGQ9Ik0xMiAtNDBjMCAwIDYgMjIgLTQgMzQiLz48Y2lyY2xlIGN4PSIwIiBjeT0iMTAiIHI9IjIwIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNLTEwIDR2LTQiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Ik0xMCA0di00IiBzdHJva2Utd2lkdGg9IjQiLz48cGF0aCBkPSJNLTggMjBjNCA2IDEyIDYgMTYgMCIgc3Ryb2tlLXdpZHRoPSIzIi8+PC9nPjwvc3ZnPg==";
11
+
12
+ // src/utils/logger.ts
13
+ var DEBUG = process.env.DEBUG === "true" || process.env.NODE_ENV === "development";
14
+ var SENSITIVE_PATTERNS = [
15
+ /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
16
+ /api[_-]?key["\s:=]+[A-Za-z0-9\-._~+/]{8,}/gi,
17
+ /token["\s:=]+[A-Za-z0-9\-._~+/]{8,}/gi,
18
+ /secret["\s:=]+[A-Za-z0-9\-._~+/]{8,}/gi,
19
+ /password["\s:=]+\S+/gi
20
+ ];
21
+ function sanitizeLogMessage(message) {
22
+ let sanitized = message;
23
+ for (const pattern of SENSITIVE_PATTERNS) {
24
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
25
+ }
26
+ return sanitized;
27
+ }
28
+ function log(message) {
29
+ console.error(`[openclaw-mcp] ${sanitizeLogMessage(message)}`);
30
+ }
31
+ function logError(message, error) {
32
+ console.error(`[openclaw-mcp] ERROR: ${sanitizeLogMessage(message)}`);
33
+ if (error) {
34
+ if (error instanceof Error) {
35
+ console.error(`[openclaw-mcp] ${sanitizeLogMessage(error.message)}`);
36
+ } else {
37
+ console.error("[openclaw-mcp] (non-Error object thrown)");
38
+ }
39
+ }
40
+ }
41
+
42
+ // src/cli.ts
43
+ import yargs from "yargs";
44
+ import { hideBin } from "yargs/helpers";
45
+ function parseArguments(version) {
46
+ const argv = yargs(hideBin(process.argv)).version(version).option("openclaw-url", {
47
+ alias: "u",
48
+ type: "string",
49
+ description: "OpenClaw gateway URL",
50
+ default: process.env.OPENCLAW_URL || DEFAULT_OPENCLAW_URL
51
+ }).option("gateway-token", {
52
+ type: "string",
53
+ description: "Bearer token for OpenClaw gateway authentication",
54
+ default: process.env.OPENCLAW_GATEWAY_TOKEN || void 0
55
+ }).option("transport", {
56
+ alias: "t",
57
+ type: "string",
58
+ choices: ["stdio", "sse"],
59
+ description: "Transport mode (stdio for local, sse for remote)",
60
+ default: "stdio"
61
+ }).option("port", {
62
+ alias: "p",
63
+ type: "number",
64
+ description: "Port for SSE server",
65
+ default: parseInt(process.env.PORT || "3000", 10)
66
+ }).option("host", {
67
+ type: "string",
68
+ description: "Host for SSE server",
69
+ default: process.env.HOST || "0.0.0.0"
70
+ }).option("auth", {
71
+ type: "boolean",
72
+ description: "Enable OAuth authentication (SSE mode)",
73
+ default: process.env.AUTH_ENABLED === "true" || process.env.OAUTH_ENABLED === "true"
74
+ }).option("client-id", {
75
+ type: "string",
76
+ description: "MCP OAuth client ID",
77
+ default: process.env.MCP_CLIENT_ID || void 0
78
+ }).option("client-secret", {
79
+ type: "string",
80
+ description: "MCP OAuth client secret",
81
+ default: process.env.MCP_CLIENT_SECRET || void 0
82
+ }).option("issuer-url", {
83
+ type: "string",
84
+ description: "OAuth issuer URL (for HTTPS behind reverse proxy)",
85
+ default: process.env.MCP_ISSUER_URL || void 0
86
+ }).option("redirect-uris", {
87
+ type: "string",
88
+ description: "Allowed OAuth redirect URIs (comma-separated)",
89
+ default: process.env.MCP_REDIRECT_URIS || void 0
90
+ }).help().parseSync();
91
+ return {
92
+ openclawUrl: argv["openclaw-url"],
93
+ gatewayToken: argv["gateway-token"],
94
+ transport: argv.transport,
95
+ port: argv.port,
96
+ host: argv.host,
97
+ authEnabled: argv.auth,
98
+ clientId: argv["client-id"],
99
+ clientSecret: argv["client-secret"],
100
+ issuerUrl: argv["issuer-url"],
101
+ redirectUris: argv["redirect-uris"] ? argv["redirect-uris"].split(",").map((s) => s.trim()).filter(Boolean) : void 0
102
+ };
103
+ }
104
+
105
+ // src/utils/errors.ts
106
+ var OpenClawError = class extends Error {
107
+ constructor(message) {
108
+ super(message);
109
+ this.name = "OpenClawError";
110
+ }
111
+ };
112
+ var OpenClawConnectionError = class extends OpenClawError {
113
+ constructor(message) {
114
+ super(message);
115
+ this.name = "OpenClawConnectionError";
116
+ }
117
+ };
118
+ var OpenClawApiError = class extends OpenClawError {
119
+ statusCode;
120
+ constructor(message, statusCode) {
121
+ super(message);
122
+ this.name = "OpenClawApiError";
123
+ this.statusCode = statusCode;
124
+ }
125
+ };
126
+
127
+ // src/openclaw/client.ts
128
+ var DEFAULT_TIMEOUT_MS = 3e4;
129
+ var MAX_RESPONSE_SIZE_BYTES = 10 * 1024 * 1024;
130
+ var OpenClawClient = class {
131
+ baseUrl;
132
+ gatewayToken;
133
+ timeoutMs;
134
+ constructor(baseUrl, gatewayToken, timeoutMs = DEFAULT_TIMEOUT_MS) {
135
+ this.baseUrl = baseUrl.replace(/\/$/, "");
136
+ this.gatewayToken = gatewayToken;
137
+ this.timeoutMs = timeoutMs;
138
+ }
139
+ buildHeaders() {
140
+ const headers = {
141
+ "Content-Type": "application/json"
142
+ };
143
+ if (this.gatewayToken) {
144
+ headers["Authorization"] = `Bearer ${this.gatewayToken}`;
145
+ }
146
+ return headers;
147
+ }
148
+ async request(path, options = {}) {
149
+ const url = `${this.baseUrl}${path}`;
150
+ const controller = new AbortController();
151
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
152
+ try {
153
+ const response = await fetch(url, {
154
+ ...options,
155
+ signal: controller.signal,
156
+ headers: {
157
+ ...this.buildHeaders(),
158
+ ...options.headers || {}
159
+ }
160
+ });
161
+ if (!response.ok) {
162
+ throw new OpenClawApiError(
163
+ `API request failed: ${response.status} ${response.statusText}`,
164
+ response.status
165
+ );
166
+ }
167
+ const contentLength = response.headers.get("content-length");
168
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE_BYTES) {
169
+ throw new OpenClawApiError("Response exceeds maximum allowed size (10MB)", 413);
170
+ }
171
+ const text = await response.text();
172
+ if (text.length > MAX_RESPONSE_SIZE_BYTES) {
173
+ throw new OpenClawApiError("Response exceeds maximum allowed size (10MB)", 413);
174
+ }
175
+ return JSON.parse(text);
176
+ } catch (error) {
177
+ if (error instanceof OpenClawApiError) {
178
+ throw error;
179
+ }
180
+ if (error instanceof DOMException && error.name === "AbortError") {
181
+ throw new OpenClawConnectionError(
182
+ `Request to OpenClaw timed out after ${this.timeoutMs}ms`
183
+ );
184
+ }
185
+ throw new OpenClawConnectionError(
186
+ `Failed to connect to OpenClaw at ${this.baseUrl}: ${error instanceof Error ? error.message : "Unknown error"}`
187
+ );
188
+ } finally {
189
+ clearTimeout(timeout);
190
+ }
191
+ }
192
+ /**
193
+ * Check gateway health by sending a minimal chat completion request.
194
+ * A 400 Bad Request means the gateway is alive (it parsed JSON, rejected input).
195
+ * A successful response also means healthy.
196
+ * Connection errors mean the gateway is down.
197
+ */
198
+ async health() {
199
+ const url = `${this.baseUrl}/v1/chat/completions`;
200
+ const controller = new AbortController();
201
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
202
+ try {
203
+ const response = await fetch(url, {
204
+ method: "POST",
205
+ signal: controller.signal,
206
+ headers: this.buildHeaders(),
207
+ body: JSON.stringify({
208
+ model: "health-check",
209
+ messages: [],
210
+ max_tokens: 1
211
+ })
212
+ });
213
+ if (response.status >= 200 && response.status < 500) {
214
+ return { status: "ok", message: `Gateway responding (HTTP ${response.status})` };
215
+ }
216
+ return { status: "error", message: `Gateway error (HTTP ${response.status})` };
217
+ } catch (error) {
218
+ if (error instanceof DOMException && error.name === "AbortError") {
219
+ throw new OpenClawConnectionError(
220
+ `Request to OpenClaw timed out after ${this.timeoutMs}ms`
221
+ );
222
+ }
223
+ throw new OpenClawConnectionError(
224
+ `Failed to connect to OpenClaw at ${this.baseUrl}: ${error instanceof Error ? error.message : "Unknown error"}`
225
+ );
226
+ } finally {
227
+ clearTimeout(timeout);
228
+ }
229
+ }
230
+ /**
231
+ * Send a chat message via the OpenAI-compatible /v1/chat/completions endpoint.
232
+ */
233
+ async chat(message, _sessionId) {
234
+ const completion = await this.request("/v1/chat/completions", {
235
+ method: "POST",
236
+ body: JSON.stringify({
237
+ model: "claude-opus-4-5",
238
+ messages: [{ role: "user", content: message }],
239
+ max_tokens: 4096
240
+ })
241
+ });
242
+ const content = completion.choices?.[0]?.message?.content ?? "";
243
+ return {
244
+ response: content,
245
+ model: completion.model,
246
+ usage: completion.usage
247
+ };
248
+ }
249
+ };
250
+
251
+ // src/server/tools-registration.ts
252
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
253
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
254
+
255
+ // src/utils/response-helpers.ts
256
+ function successResponse(text) {
257
+ return {
258
+ content: [{ type: "text", text }]
259
+ };
260
+ }
261
+ function errorResponse(message) {
262
+ return {
263
+ content: [{ type: "text", text: `Error: ${message}` }],
264
+ isError: true
265
+ };
266
+ }
267
+ function jsonResponse(data) {
268
+ return successResponse(JSON.stringify(data, null, 2));
269
+ }
270
+
271
+ // src/utils/validation.ts
272
+ var CONTROL_CHAR_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/;
273
+ var MAX_MESSAGE_LENGTH = 1e5;
274
+ var MAX_ID_LENGTH = 256;
275
+ function validateInputIsObject(input) {
276
+ return input !== null && input !== void 0 && typeof input === "object" && !Array.isArray(input);
277
+ }
278
+ function validateString(value, fieldName, maxLength) {
279
+ if (typeof value !== "string") {
280
+ return { valid: false, value: "", error: `${fieldName} must be a string` };
281
+ }
282
+ const trimmed = value.trim();
283
+ if (trimmed.length === 0) {
284
+ return { valid: false, value: "", error: `${fieldName} must not be empty` };
285
+ }
286
+ if (trimmed.length > maxLength) {
287
+ return {
288
+ valid: false,
289
+ value: "",
290
+ error: `${fieldName} exceeds maximum length of ${maxLength} characters`
291
+ };
292
+ }
293
+ if (CONTROL_CHAR_RE.test(trimmed)) {
294
+ return { valid: false, value: "", error: `${fieldName} contains invalid control characters` };
295
+ }
296
+ return { valid: true, value: trimmed, error: "" };
297
+ }
298
+ function validateMessage(value) {
299
+ return validateString(value, "message", MAX_MESSAGE_LENGTH);
300
+ }
301
+ function validateId(value, fieldName) {
302
+ return validateString(value, fieldName, MAX_ID_LENGTH);
303
+ }
304
+
305
+ // src/mcp/tools/chat.ts
306
+ var openclawChatTool = {
307
+ name: "openclaw_chat",
308
+ description: "Send a message to OpenClaw and get a response",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ message: {
313
+ type: "string",
314
+ description: "The message to send to OpenClaw"
315
+ },
316
+ session_id: {
317
+ type: "string",
318
+ description: "Optional session ID for conversation context"
319
+ }
320
+ },
321
+ required: ["message"]
322
+ }
323
+ };
324
+ async function handleOpenclawChat(client2, input) {
325
+ if (!validateInputIsObject(input)) {
326
+ return errorResponse("Invalid input: expected an object");
327
+ }
328
+ const msgResult = validateMessage(input.message);
329
+ if (msgResult.valid === false) {
330
+ return errorResponse(msgResult.error);
331
+ }
332
+ let sessionId;
333
+ if (input.session_id !== void 0) {
334
+ const sidResult = validateId(input.session_id, "session_id");
335
+ if (sidResult.valid === false) {
336
+ return errorResponse(sidResult.error);
337
+ }
338
+ sessionId = sidResult.value;
339
+ }
340
+ try {
341
+ const response = await client2.chat(msgResult.value, sessionId);
342
+ return successResponse(response.response);
343
+ } catch (error) {
344
+ return errorResponse(error instanceof Error ? error.message : "Failed to chat with OpenClaw");
345
+ }
346
+ }
347
+
348
+ // src/mcp/tools/status.ts
349
+ var openclawStatusTool = {
350
+ name: "openclaw_status",
351
+ description: "Get OpenClaw gateway status and health information",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {}
355
+ }
356
+ };
357
+ async function handleOpenclawStatus(client2, input) {
358
+ if (!validateInputIsObject(input)) {
359
+ return errorResponse("Invalid input: expected an object");
360
+ }
361
+ try {
362
+ const response = await client2.health();
363
+ return jsonResponse(response);
364
+ } catch (error) {
365
+ return errorResponse(
366
+ error instanceof Error ? error.message : "Failed to get status from OpenClaw"
367
+ );
368
+ }
369
+ }
370
+
371
+ // src/mcp/tasks/manager.ts
372
+ var MAX_TASKS = 1e3;
373
+ var CLEANUP_INTERVAL_MS = 10 * 60 * 1e3;
374
+ var CLEANUP_MAX_AGE_MS = 60 * 60 * 1e3;
375
+ var TaskManager = class {
376
+ tasks = /* @__PURE__ */ new Map();
377
+ taskCounter = 0;
378
+ cleanupInterval;
379
+ constructor() {
380
+ this.cleanupInterval = setInterval(() => this.cleanup(CLEANUP_MAX_AGE_MS), CLEANUP_INTERVAL_MS);
381
+ if (this.cleanupInterval.unref) {
382
+ this.cleanupInterval.unref();
383
+ }
384
+ }
385
+ /**
386
+ * Generate unique task ID
387
+ */
388
+ generateId() {
389
+ this.taskCounter++;
390
+ const timestamp = Date.now().toString(36);
391
+ const counter = this.taskCounter.toString(36).padStart(4, "0");
392
+ return `task_${timestamp}_${counter}`;
393
+ }
394
+ /**
395
+ * Create a new task
396
+ */
397
+ create(options) {
398
+ if (this.tasks.size >= MAX_TASKS) {
399
+ throw new Error(
400
+ `Task limit reached (${MAX_TASKS}). Wait for tasks to complete or cancel pending ones.`
401
+ );
402
+ }
403
+ const id = this.generateId();
404
+ const task = {
405
+ id,
406
+ type: options.type,
407
+ status: "pending",
408
+ input: options.input,
409
+ createdAt: /* @__PURE__ */ new Date(),
410
+ sessionId: options.sessionId,
411
+ priority: options.priority ?? 0
412
+ };
413
+ this.tasks.set(id, task);
414
+ log(`Task created: ${id} (type: ${task.type})`);
415
+ return task;
416
+ }
417
+ /**
418
+ * Get task by ID
419
+ */
420
+ get(id) {
421
+ return this.tasks.get(id);
422
+ }
423
+ /**
424
+ * List all tasks, optionally filtered by status
425
+ */
426
+ list(filter) {
427
+ let tasks = Array.from(this.tasks.values());
428
+ if (filter?.status) {
429
+ tasks = tasks.filter((t) => t.status === filter.status);
430
+ }
431
+ if (filter?.sessionId) {
432
+ tasks = tasks.filter((t) => t.sessionId === filter.sessionId);
433
+ }
434
+ return tasks.sort((a, b) => {
435
+ if (b.priority !== a.priority) return b.priority - a.priority;
436
+ return a.createdAt.getTime() - b.createdAt.getTime();
437
+ });
438
+ }
439
+ /**
440
+ * Update task status
441
+ */
442
+ updateStatus(id, status, result, error) {
443
+ const task = this.tasks.get(id);
444
+ if (!task) return false;
445
+ task.status = status;
446
+ if (status === "running" && !task.startedAt) {
447
+ task.startedAt = /* @__PURE__ */ new Date();
448
+ }
449
+ if (status === "completed" || status === "failed" || status === "cancelled") {
450
+ task.completedAt = /* @__PURE__ */ new Date();
451
+ }
452
+ if (result !== void 0) task.result = result;
453
+ if (error !== void 0) task.error = error;
454
+ log(`Task ${id} status: ${status}`);
455
+ return true;
456
+ }
457
+ /**
458
+ * Cancel a pending task
459
+ */
460
+ cancel(id) {
461
+ const task = this.tasks.get(id);
462
+ if (!task) return false;
463
+ if (task.status !== "pending") {
464
+ return false;
465
+ }
466
+ task.status = "cancelled";
467
+ task.completedAt = /* @__PURE__ */ new Date();
468
+ log(`Task cancelled: ${id}`);
469
+ return true;
470
+ }
471
+ /**
472
+ * Delete a task (cleanup)
473
+ */
474
+ delete(id) {
475
+ return this.tasks.delete(id);
476
+ }
477
+ /**
478
+ * Get next pending task (for workers)
479
+ */
480
+ getNextPending() {
481
+ const pending = this.list({ status: "pending" });
482
+ return pending[0];
483
+ }
484
+ /**
485
+ * Clean up old completed/failed tasks
486
+ */
487
+ cleanup(maxAgeMs = 36e5) {
488
+ const now = Date.now();
489
+ let cleaned = 0;
490
+ for (const [id, task] of this.tasks) {
491
+ if (task.completedAt && now - task.completedAt.getTime() > maxAgeMs) {
492
+ this.tasks.delete(id);
493
+ cleaned++;
494
+ }
495
+ }
496
+ if (cleaned > 0) {
497
+ log(`Cleaned up ${cleaned} old tasks`);
498
+ }
499
+ return cleaned;
500
+ }
501
+ /**
502
+ * Get statistics
503
+ */
504
+ stats() {
505
+ const byStatus = {
506
+ pending: 0,
507
+ running: 0,
508
+ completed: 0,
509
+ failed: 0,
510
+ cancelled: 0
511
+ };
512
+ for (const task of this.tasks.values()) {
513
+ byStatus[task.status]++;
514
+ }
515
+ return {
516
+ total: this.tasks.size,
517
+ byStatus
518
+ };
519
+ }
520
+ };
521
+ var taskManager = new TaskManager();
522
+
523
+ // src/mcp/tools/tasks.ts
524
+ var openclawChatAsyncTool = {
525
+ name: "openclaw_chat_async",
526
+ description: "Send a message to OpenClaw asynchronously. Returns a task_id immediately that can be polled for results. Use this for potentially long-running conversations.",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {
530
+ message: {
531
+ type: "string",
532
+ description: "The message to send to OpenClaw"
533
+ },
534
+ session_id: {
535
+ type: "string",
536
+ description: "Optional session ID for conversation context"
537
+ },
538
+ priority: {
539
+ type: "number",
540
+ description: "Task priority (higher = processed first). Default: 0"
541
+ }
542
+ },
543
+ required: ["message"]
544
+ }
545
+ };
546
+ var openclawTaskStatusTool = {
547
+ name: "openclaw_task_status",
548
+ description: "Check the status of an async task. Returns status, and result if completed.",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ task_id: {
553
+ type: "string",
554
+ description: "The task ID returned from openclaw_chat_async"
555
+ }
556
+ },
557
+ required: ["task_id"]
558
+ }
559
+ };
560
+ var openclawTaskListTool = {
561
+ name: "openclaw_task_list",
562
+ description: "List all tasks. Optionally filter by status or session.",
563
+ inputSchema: {
564
+ type: "object",
565
+ properties: {
566
+ status: {
567
+ type: "string",
568
+ enum: ["pending", "running", "completed", "failed", "cancelled"],
569
+ description: "Filter by task status"
570
+ },
571
+ session_id: {
572
+ type: "string",
573
+ description: "Filter by session ID"
574
+ }
575
+ },
576
+ required: []
577
+ }
578
+ };
579
+ var openclawTaskCancelTool = {
580
+ name: "openclaw_task_cancel",
581
+ description: "Cancel a pending task. Only works for tasks that haven't started yet.",
582
+ inputSchema: {
583
+ type: "object",
584
+ properties: {
585
+ task_id: {
586
+ type: "string",
587
+ description: "The task ID to cancel"
588
+ }
589
+ },
590
+ required: ["task_id"]
591
+ }
592
+ };
593
+ var processorRunning = false;
594
+ var processorClient = null;
595
+ async function processTask(task, client2) {
596
+ taskManager.updateStatus(task.id, "running");
597
+ try {
598
+ if (task.type === "chat") {
599
+ const input = task.input;
600
+ const response = await client2.chat(input.message, input.session_id);
601
+ taskManager.updateStatus(task.id, "completed", response.response);
602
+ } else {
603
+ taskManager.updateStatus(task.id, "failed", void 0, "Unknown task type");
604
+ }
605
+ } catch (error) {
606
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
607
+ taskManager.updateStatus(task.id, "failed", void 0, errorMsg);
608
+ }
609
+ }
610
+ async function taskProcessor() {
611
+ if (!processorClient) return;
612
+ while (processorRunning) {
613
+ const task = taskManager.getNextPending();
614
+ if (task) {
615
+ await processTask(task, processorClient);
616
+ } else {
617
+ await new Promise((resolve) => setTimeout(resolve, 100));
618
+ }
619
+ }
620
+ }
621
+ function startTaskProcessor(client2) {
622
+ if (processorRunning) return;
623
+ processorClient = client2;
624
+ processorRunning = true;
625
+ taskProcessor().catch(() => {
626
+ processorRunning = false;
627
+ });
628
+ log("Task processor started");
629
+ }
630
+ async function handleOpenclawChatAsync(client2, input) {
631
+ if (!validateInputIsObject(input)) {
632
+ return errorResponse("Invalid input: expected an object");
633
+ }
634
+ const msgResult = validateMessage(input.message);
635
+ if (msgResult.valid === false) {
636
+ return errorResponse(msgResult.error);
637
+ }
638
+ let sessionId;
639
+ if (input.session_id !== void 0) {
640
+ const sidResult = validateId(input.session_id, "session_id");
641
+ if (sidResult.valid === false) {
642
+ return errorResponse(sidResult.error);
643
+ }
644
+ sessionId = sidResult.value;
645
+ }
646
+ let priority = 0;
647
+ if (input.priority !== void 0) {
648
+ if (typeof input.priority !== "number" || !Number.isInteger(input.priority)) {
649
+ return errorResponse("priority must be an integer");
650
+ }
651
+ priority = input.priority;
652
+ }
653
+ startTaskProcessor(client2);
654
+ const task = taskManager.create({
655
+ type: "chat",
656
+ input: { message: msgResult.value, session_id: sessionId },
657
+ sessionId,
658
+ priority
659
+ });
660
+ return successResponse(
661
+ JSON.stringify(
662
+ {
663
+ task_id: task.id,
664
+ status: task.status,
665
+ message: "Task queued. Use openclaw_task_status to check progress."
666
+ },
667
+ null,
668
+ 2
669
+ )
670
+ );
671
+ }
672
+ async function handleOpenclawTaskStatus(_client, input) {
673
+ if (!validateInputIsObject(input)) {
674
+ return errorResponse("Invalid input: expected an object");
675
+ }
676
+ const tidResult = validateId(input.task_id, "task_id");
677
+ if (tidResult.valid === false) {
678
+ return errorResponse(tidResult.error);
679
+ }
680
+ const task_id = tidResult.value;
681
+ const task = taskManager.get(task_id);
682
+ if (!task) {
683
+ return errorResponse(`Task not found: ${task_id}`);
684
+ }
685
+ const response = {
686
+ task_id: task.id,
687
+ type: task.type,
688
+ status: task.status,
689
+ created_at: task.createdAt.toISOString()
690
+ };
691
+ if (task.startedAt) {
692
+ response.started_at = task.startedAt.toISOString();
693
+ }
694
+ if (task.completedAt) {
695
+ response.completed_at = task.completedAt.toISOString();
696
+ }
697
+ if (task.status === "completed" && task.result) {
698
+ response.result = task.result;
699
+ }
700
+ if (task.status === "failed" && task.error) {
701
+ response.error = task.error;
702
+ }
703
+ return successResponse(JSON.stringify(response, null, 2));
704
+ }
705
+ var VALID_TASK_STATUSES = ["pending", "running", "completed", "failed", "cancelled"];
706
+ async function handleOpenclawTaskList(_client, input) {
707
+ if (!validateInputIsObject(input)) {
708
+ return errorResponse("Invalid input: expected an object");
709
+ }
710
+ let status;
711
+ if (input.status !== void 0) {
712
+ if (typeof input.status !== "string" || !VALID_TASK_STATUSES.includes(input.status)) {
713
+ return errorResponse(`status must be one of: ${VALID_TASK_STATUSES.join(", ")}`);
714
+ }
715
+ status = input.status;
716
+ }
717
+ let session_id;
718
+ if (input.session_id !== void 0) {
719
+ const sidResult = validateId(input.session_id, "session_id");
720
+ if (sidResult.valid === false) {
721
+ return errorResponse(sidResult.error);
722
+ }
723
+ session_id = sidResult.value;
724
+ }
725
+ const tasks = taskManager.list({ status, sessionId: session_id });
726
+ const stats = taskManager.stats();
727
+ const taskList = tasks.map((t) => ({
728
+ task_id: t.id,
729
+ type: t.type,
730
+ status: t.status,
731
+ priority: t.priority,
732
+ created_at: t.createdAt.toISOString(),
733
+ has_result: t.status === "completed" && !!t.result
734
+ }));
735
+ return successResponse(
736
+ JSON.stringify(
737
+ {
738
+ stats,
739
+ tasks: taskList
740
+ },
741
+ null,
742
+ 2
743
+ )
744
+ );
745
+ }
746
+ async function handleOpenclawTaskCancel(_client, input) {
747
+ if (!validateInputIsObject(input)) {
748
+ return errorResponse("Invalid input: expected an object");
749
+ }
750
+ const tidResult = validateId(input.task_id, "task_id");
751
+ if (tidResult.valid === false) {
752
+ return errorResponse(tidResult.error);
753
+ }
754
+ const task_id = tidResult.value;
755
+ const task = taskManager.get(task_id);
756
+ if (!task) {
757
+ return errorResponse(`Task not found: ${task_id}`);
758
+ }
759
+ if (task.status !== "pending") {
760
+ return errorResponse(
761
+ `Cannot cancel task with status: ${task.status}. Only pending tasks can be cancelled.`
762
+ );
763
+ }
764
+ const cancelled = taskManager.cancel(task_id);
765
+ if (!cancelled) {
766
+ return errorResponse("Failed to cancel task");
767
+ }
768
+ return successResponse(
769
+ JSON.stringify(
770
+ {
771
+ task_id,
772
+ status: "cancelled",
773
+ message: "Task cancelled successfully"
774
+ },
775
+ null,
776
+ 2
777
+ )
778
+ );
779
+ }
780
+
781
+ // src/server/tools-registration.ts
782
+ function createMcpServer(deps2) {
783
+ const server = new Server(
784
+ {
785
+ name: deps2.serverName,
786
+ version: deps2.serverVersion,
787
+ icons: [
788
+ {
789
+ src: `data:image/svg+xml;base64,${SERVER_ICON_SVG_BASE64}`,
790
+ mimeType: "image/svg+xml",
791
+ sizes: ["128x128"]
792
+ }
793
+ ]
794
+ },
795
+ { capabilities: { tools: {} } }
796
+ );
797
+ registerTools(server, deps2);
798
+ return server;
799
+ }
800
+ function registerTools(server, deps2) {
801
+ const { client: client2 } = deps2;
802
+ const toolHandlers = /* @__PURE__ */ new Map([
803
+ ["openclaw_chat", (input) => handleOpenclawChat(client2, input)],
804
+ ["openclaw_status", (input) => handleOpenclawStatus(client2, input)],
805
+ ["openclaw_chat_async", (input) => handleOpenclawChatAsync(client2, input)],
806
+ ["openclaw_task_status", (input) => handleOpenclawTaskStatus(client2, input)],
807
+ ["openclaw_task_list", (input) => handleOpenclawTaskList(client2, input)],
808
+ ["openclaw_task_cancel", (input) => handleOpenclawTaskCancel(client2, input)]
809
+ ]);
810
+ const allTools = [
811
+ openclawChatTool,
812
+ openclawStatusTool,
813
+ openclawChatAsyncTool,
814
+ openclawTaskStatusTool,
815
+ openclawTaskListTool,
816
+ openclawTaskCancelTool
817
+ ];
818
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
819
+ return { tools: allTools };
820
+ });
821
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
822
+ const { name, arguments: toolArgs } = request.params;
823
+ log(`Executing tool: ${name}`);
824
+ const handler = toolHandlers.get(name);
825
+ if (!handler) {
826
+ throw new Error(`Unknown tool: ${name}`);
827
+ }
828
+ try {
829
+ return await handler(toolArgs);
830
+ } catch (error) {
831
+ logError(`Error executing tool ${name}`, error);
832
+ throw error;
833
+ }
834
+ });
835
+ }
836
+
837
+ // src/server/sse.ts
838
+ import { randomUUID as randomUUID2 } from "crypto";
839
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
840
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
841
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
842
+ import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
843
+ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
844
+
845
+ // src/auth/provider.ts
846
+ import { randomUUID } from "crypto";
847
+ import {
848
+ InvalidRequestError,
849
+ InvalidTokenError
850
+ } from "@modelcontextprotocol/sdk/server/auth/errors.js";
851
+ var TOKEN_TTL_MS = 60 * 60 * 1e3;
852
+ var AUTH_CODE_TTL_MS = 10 * 60 * 1e3;
853
+ var REFRESH_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
854
+ var REAPER_INTERVAL_MS = 5 * 60 * 1e3;
855
+ var ALLOW_ANY_REDIRECT = new Proxy([], {
856
+ get(target, prop) {
857
+ if (prop === "includes") return () => true;
858
+ if (prop === "length") return 1;
859
+ return Reflect.get(target, prop);
860
+ }
861
+ });
862
+ var OpenClawClientsStore = class {
863
+ client;
864
+ constructor(config) {
865
+ if (config.clientId && config.clientSecret) {
866
+ const redirectUris = config.redirectUris && config.redirectUris.length > 0 ? config.redirectUris : ALLOW_ANY_REDIRECT;
867
+ this.client = {
868
+ client_id: config.clientId,
869
+ client_secret: config.clientSecret,
870
+ redirect_uris: redirectUris,
871
+ token_endpoint_auth_method: "client_secret_post",
872
+ grant_types: ["authorization_code", "refresh_token"],
873
+ response_types: ["code"],
874
+ client_name: "OpenClaw MCP Client",
875
+ client_id_issued_at: Math.floor(Date.now() / 1e3)
876
+ };
877
+ }
878
+ }
879
+ async getClient(clientId) {
880
+ if (this.client && this.client.client_id === clientId) {
881
+ return this.client;
882
+ }
883
+ return void 0;
884
+ }
885
+ // No registerClient — dynamic client registration is intentionally disabled.
886
+ // Only the pre-configured client from MCP_CLIENT_ID + MCP_CLIENT_SECRET can authenticate.
887
+ // This prevents anyone who knows the server URL from self-registering and bypassing auth.
888
+ };
889
+ var OpenClawAuthProvider = class {
890
+ clientsStore;
891
+ codes = /* @__PURE__ */ new Map();
892
+ tokens = /* @__PURE__ */ new Map();
893
+ refreshTokens = /* @__PURE__ */ new Map();
894
+ reaperInterval;
895
+ constructor(config) {
896
+ this.clientsStore = new OpenClawClientsStore(config);
897
+ this.reaperInterval = setInterval(() => this.reapExpired(), REAPER_INTERVAL_MS);
898
+ if (this.reaperInterval.unref) {
899
+ this.reaperInterval.unref();
900
+ }
901
+ }
902
+ /**
903
+ * Clean up expired auth codes, access tokens, and refresh tokens.
904
+ */
905
+ reapExpired() {
906
+ const now = Date.now();
907
+ for (const [code, data] of this.codes) {
908
+ if (now - data.createdAt > AUTH_CODE_TTL_MS) {
909
+ this.codes.delete(code);
910
+ }
911
+ }
912
+ for (const [token, data] of this.tokens) {
913
+ if (data.expiresAt < now) {
914
+ this.tokens.delete(token);
915
+ }
916
+ }
917
+ for (const [token, data] of this.refreshTokens) {
918
+ if (data.expiresAt < now) {
919
+ this.refreshTokens.delete(token);
920
+ }
921
+ }
922
+ }
923
+ /**
924
+ * Auto-approve: generate auth code and redirect immediately.
925
+ */
926
+ async authorize(client2, params, res) {
927
+ const code = randomUUID();
928
+ this.codes.set(code, { client: client2, params, createdAt: Date.now() });
929
+ const searchParams = new URLSearchParams({ code });
930
+ if (params.state !== void 0) {
931
+ searchParams.set("state", params.state);
932
+ }
933
+ const targetUrl = new URL(params.redirectUri);
934
+ targetUrl.search = searchParams.toString();
935
+ res.redirect(targetUrl.toString());
936
+ }
937
+ async challengeForAuthorizationCode(_client, authorizationCode) {
938
+ const codeData = this.codes.get(authorizationCode);
939
+ if (!codeData || Date.now() - codeData.createdAt > AUTH_CODE_TTL_MS) {
940
+ if (codeData) this.codes.delete(authorizationCode);
941
+ throw new InvalidRequestError("Invalid authorization code");
942
+ }
943
+ return codeData.params.codeChallenge;
944
+ }
945
+ async exchangeAuthorizationCode(client2, authorizationCode, _codeVerifier, _redirectUri, resource) {
946
+ const codeData = this.codes.get(authorizationCode);
947
+ if (!codeData || Date.now() - codeData.createdAt > AUTH_CODE_TTL_MS) {
948
+ if (codeData) this.codes.delete(authorizationCode);
949
+ throw new InvalidRequestError("Invalid authorization code");
950
+ }
951
+ if (codeData.client.client_id !== client2.client_id) {
952
+ throw new InvalidRequestError("Authorization code was not issued to this client");
953
+ }
954
+ this.codes.delete(authorizationCode);
955
+ const accessToken = randomUUID();
956
+ const refreshToken = randomUUID();
957
+ const scopes = codeData.params.scopes || [];
958
+ this.tokens.set(accessToken, {
959
+ token: accessToken,
960
+ clientId: client2.client_id,
961
+ scopes,
962
+ expiresAt: Date.now() + TOKEN_TTL_MS,
963
+ resource: resource || codeData.params.resource
964
+ });
965
+ this.refreshTokens.set(refreshToken, {
966
+ clientId: client2.client_id,
967
+ scopes,
968
+ expiresAt: Date.now() + REFRESH_TOKEN_TTL_MS,
969
+ resource: resource || codeData.params.resource
970
+ });
971
+ return {
972
+ access_token: accessToken,
973
+ token_type: "bearer",
974
+ expires_in: TOKEN_TTL_MS / 1e3,
975
+ refresh_token: refreshToken,
976
+ scope: scopes.join(" ")
977
+ };
978
+ }
979
+ async exchangeRefreshToken(client2, refreshToken, scopes, resource) {
980
+ const data = this.refreshTokens.get(refreshToken);
981
+ if (!data || data.expiresAt < Date.now()) {
982
+ if (data) this.refreshTokens.delete(refreshToken);
983
+ throw new InvalidRequestError("Invalid refresh token");
984
+ }
985
+ if (data.clientId !== client2.client_id) {
986
+ throw new InvalidRequestError("Refresh token was not issued to this client");
987
+ }
988
+ this.refreshTokens.delete(refreshToken);
989
+ const accessToken = randomUUID();
990
+ const newRefreshToken = randomUUID();
991
+ const tokenScopes = scopes || data.scopes;
992
+ this.tokens.set(accessToken, {
993
+ token: accessToken,
994
+ clientId: client2.client_id,
995
+ scopes: tokenScopes,
996
+ expiresAt: Date.now() + TOKEN_TTL_MS,
997
+ resource: resource || data.resource
998
+ });
999
+ this.refreshTokens.set(newRefreshToken, {
1000
+ clientId: client2.client_id,
1001
+ scopes: tokenScopes,
1002
+ expiresAt: Date.now() + REFRESH_TOKEN_TTL_MS,
1003
+ resource: resource || data.resource
1004
+ });
1005
+ return {
1006
+ access_token: accessToken,
1007
+ token_type: "bearer",
1008
+ expires_in: TOKEN_TTL_MS / 1e3,
1009
+ refresh_token: newRefreshToken,
1010
+ scope: tokenScopes.join(" ")
1011
+ };
1012
+ }
1013
+ async verifyAccessToken(token) {
1014
+ const tokenData = this.tokens.get(token);
1015
+ if (!tokenData || tokenData.expiresAt < Date.now()) {
1016
+ throw new InvalidTokenError("Invalid or expired token");
1017
+ }
1018
+ return {
1019
+ token,
1020
+ clientId: tokenData.clientId,
1021
+ scopes: tokenData.scopes,
1022
+ expiresAt: Math.floor(tokenData.expiresAt / 1e3),
1023
+ resource: tokenData.resource
1024
+ };
1025
+ }
1026
+ async revokeToken(_client, request) {
1027
+ this.tokens.delete(request.token);
1028
+ this.refreshTokens.delete(request.token);
1029
+ }
1030
+ };
1031
+
1032
+ // src/server/sse.ts
1033
+ function loadCorsConfig() {
1034
+ const corsOrigins = process.env.CORS_ORIGINS;
1035
+ if (!corsOrigins || corsOrigins === "*") {
1036
+ return { origins: ["*"], enabled: true };
1037
+ }
1038
+ if (corsOrigins.toLowerCase() === "none" || corsOrigins === "") {
1039
+ return { origins: [], enabled: false };
1040
+ }
1041
+ return {
1042
+ origins: corsOrigins.split(",").map((s) => s.trim()).filter(Boolean),
1043
+ enabled: true
1044
+ };
1045
+ }
1046
+ function isOriginAllowed(origin, allowedOrigins) {
1047
+ if (!origin) return false;
1048
+ if (allowedOrigins.includes("*")) return true;
1049
+ return allowedOrigins.some((allowed) => {
1050
+ if (allowed.startsWith("*.")) {
1051
+ const domain = allowed.slice(1);
1052
+ try {
1053
+ const originHost = new URL(origin).hostname;
1054
+ return originHost === domain.slice(1) || originHost.endsWith(domain);
1055
+ } catch {
1056
+ return false;
1057
+ }
1058
+ }
1059
+ return origin === allowed || origin === `https://${allowed}` || origin === `http://${allowed}`;
1060
+ });
1061
+ }
1062
+ async function createSSEServer(config, deps2) {
1063
+ const authEnabled = !!config.authConfig?.clientId;
1064
+ const corsConfig = loadCorsConfig();
1065
+ const sseSessions = /* @__PURE__ */ new Map();
1066
+ const streamableSessions = /* @__PURE__ */ new Map();
1067
+ const app = createMcpExpressApp({ host: config.host });
1068
+ app.use((req, res, next) => {
1069
+ if (!corsConfig.enabled) {
1070
+ next();
1071
+ return;
1072
+ }
1073
+ const origin = req.headers.origin;
1074
+ const allowedOrigin = corsConfig.origins.includes("*") ? "*" : origin && isOriginAllowed(origin, corsConfig.origins) ? origin : void 0;
1075
+ if (allowedOrigin) {
1076
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
1077
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1078
+ res.setHeader(
1079
+ "Access-Control-Allow-Headers",
1080
+ "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version"
1081
+ );
1082
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
1083
+ }
1084
+ if (req.method === "OPTIONS") {
1085
+ res.status(204).end();
1086
+ return;
1087
+ }
1088
+ next();
1089
+ });
1090
+ let authMiddleware;
1091
+ if (authEnabled) {
1092
+ const provider = new OpenClawAuthProvider(config.authConfig);
1093
+ const issuerUrl = config.issuerUrl ? new URL(config.issuerUrl) : new URL(`http://${config.host === "0.0.0.0" ? "localhost" : config.host}:${config.port}`);
1094
+ app.use(
1095
+ mcpAuthRouter({
1096
+ provider,
1097
+ issuerUrl,
1098
+ scopesSupported: ["mcp:tools"]
1099
+ })
1100
+ );
1101
+ app.get("/.well-known/oauth-protected-resource/:path", (req, res) => {
1102
+ res.json({
1103
+ resource: `${issuerUrl.toString()}${req.params.path}`,
1104
+ authorization_servers: [issuerUrl.toString().replace(/\/$/, "")],
1105
+ scopes_supported: ["mcp:tools"]
1106
+ });
1107
+ });
1108
+ authMiddleware = requireBearerAuth({ verifier: provider });
1109
+ }
1110
+ app.get("/health", (_req, res) => {
1111
+ res.json({
1112
+ status: "ok",
1113
+ transport: "sse",
1114
+ auth: authEnabled
1115
+ });
1116
+ });
1117
+ const withAuth = (handler) => {
1118
+ if (authMiddleware) {
1119
+ return [authMiddleware, async (req, res) => handler(req, res)];
1120
+ }
1121
+ return [async (req, res) => handler(req, res)];
1122
+ };
1123
+ app.get(
1124
+ "/sse",
1125
+ ...withAuth(async (req, res) => {
1126
+ const transport = new SSEServerTransport("/messages", res);
1127
+ const server = createMcpServer(deps2);
1128
+ const sessionId = transport.sessionId;
1129
+ sseSessions.set(sessionId, { transport, server });
1130
+ log(`SSE session connected: ${sessionId}`);
1131
+ transport.onclose = () => {
1132
+ sseSessions.delete(sessionId);
1133
+ log(`SSE session disconnected: ${sessionId}`);
1134
+ };
1135
+ try {
1136
+ await server.connect(transport);
1137
+ } catch (error) {
1138
+ sseSessions.delete(sessionId);
1139
+ logError(`Failed to connect SSE session ${sessionId}`, error);
1140
+ }
1141
+ })
1142
+ );
1143
+ app.post(
1144
+ "/messages",
1145
+ ...withAuth(async (req, res) => {
1146
+ const sessionId = req.query.sessionId;
1147
+ const session = sseSessions.get(sessionId);
1148
+ if (!session) {
1149
+ res.status(404).json({ error: "Session not found" });
1150
+ return;
1151
+ }
1152
+ try {
1153
+ await session.transport.handlePostMessage(
1154
+ req,
1155
+ res,
1156
+ req.body
1157
+ );
1158
+ } catch (error) {
1159
+ logError(`Error handling message for session ${sessionId}`, error);
1160
+ if (!res.headersSent) {
1161
+ res.status(500).json({ error: "Internal server error" });
1162
+ }
1163
+ }
1164
+ })
1165
+ );
1166
+ const handleStreamableRequest = async (req, res) => {
1167
+ const sessionId = req.headers["mcp-session-id"];
1168
+ if (sessionId && streamableSessions.has(sessionId)) {
1169
+ const session = streamableSessions.get(sessionId);
1170
+ try {
1171
+ await session.transport.handleRequest(
1172
+ req,
1173
+ res,
1174
+ req.body
1175
+ );
1176
+ } catch (error) {
1177
+ logError(`Error in streamable session ${sessionId}`, error);
1178
+ if (!res.headersSent) {
1179
+ res.status(500).json({ error: "Internal server error" });
1180
+ }
1181
+ }
1182
+ return;
1183
+ }
1184
+ const transport = new StreamableHTTPServerTransport({
1185
+ sessionIdGenerator: () => randomUUID2(),
1186
+ onsessioninitialized: (newSessionId) => {
1187
+ streamableSessions.set(newSessionId, { transport, server });
1188
+ log(`Streamable session initialized: ${newSessionId}`);
1189
+ }
1190
+ });
1191
+ transport.onclose = () => {
1192
+ const sid = transport.sessionId;
1193
+ if (sid) {
1194
+ streamableSessions.delete(sid);
1195
+ log(`Streamable session closed: ${sid}`);
1196
+ }
1197
+ };
1198
+ const server = createMcpServer(deps2);
1199
+ try {
1200
+ await server.connect(transport);
1201
+ await transport.handleRequest(
1202
+ req,
1203
+ res,
1204
+ req.body
1205
+ );
1206
+ } catch (error) {
1207
+ logError("Failed to initialize streamable session", error);
1208
+ if (!res.headersSent) {
1209
+ res.status(500).json({ error: "Internal server error" });
1210
+ }
1211
+ }
1212
+ };
1213
+ app.get("/mcp", ...withAuth(handleStreamableRequest));
1214
+ app.post("/mcp", ...withAuth(handleStreamableRequest));
1215
+ app.delete("/mcp", ...withAuth(handleStreamableRequest));
1216
+ const httpServer = app.listen(config.port, config.host, () => {
1217
+ log(`SSE server listening on ${config.host}:${config.port}`);
1218
+ log(`Auth enabled: ${authEnabled}`);
1219
+ log(`CORS origins: ${corsConfig.enabled ? corsConfig.origins.join(", ") : "disabled"}`);
1220
+ if (authEnabled) {
1221
+ log("OAuth 2.1 authentication is REQUIRED for all connections");
1222
+ log("Endpoints:");
1223
+ log(" GET /.well-known/oauth-authorization-server - OAuth metadata");
1224
+ log(" GET /.well-known/oauth-protected-resource/mcp - Protected resource metadata");
1225
+ log(" POST /authorize - Authorization");
1226
+ log(" POST /token - Token exchange");
1227
+ } else {
1228
+ log("WARNING: Auth is DISABLED - server is open to anyone!");
1229
+ }
1230
+ log("MCP Endpoints:");
1231
+ log(" GET /health - Health check (no auth)");
1232
+ log(" GET /sse - Legacy SSE stream");
1233
+ log(" POST /messages - Legacy SSE messages");
1234
+ log(" ALL /mcp - Streamable HTTP");
1235
+ });
1236
+ const shutdown = async () => {
1237
+ log("Shutting down SSE server...");
1238
+ for (const [id, session] of sseSessions) {
1239
+ try {
1240
+ await session.server.close();
1241
+ } catch (error) {
1242
+ logError(`Error closing SSE session ${id}`, error);
1243
+ }
1244
+ }
1245
+ sseSessions.clear();
1246
+ for (const [id, session] of streamableSessions) {
1247
+ try {
1248
+ await session.server.close();
1249
+ } catch (error) {
1250
+ logError(`Error closing streamable session ${id}`, error);
1251
+ }
1252
+ }
1253
+ streamableSessions.clear();
1254
+ httpServer.close(() => {
1255
+ log("SSE server stopped");
1256
+ process.exit(0);
1257
+ });
1258
+ setTimeout(() => {
1259
+ logError("Forced shutdown after timeout");
1260
+ process.exit(1);
1261
+ }, 5e3);
1262
+ };
1263
+ process.on("SIGTERM", shutdown);
1264
+ process.on("SIGINT", shutdown);
1265
+ }
1266
+
1267
+ // src/index.ts
1268
+ var args = parseArguments(SERVER_VERSION);
1269
+ var client = new OpenClawClient(args.openclawUrl, args.gatewayToken);
1270
+ var deps = {
1271
+ client,
1272
+ serverName: SERVER_NAME,
1273
+ serverVersion: SERVER_VERSION
1274
+ };
1275
+ async function main() {
1276
+ log(`Starting ${SERVER_NAME} v${SERVER_VERSION}`);
1277
+ log(`OpenClaw URL: ${args.openclawUrl}`);
1278
+ log(`Transport: ${args.transport}`);
1279
+ log(`Gateway token: ${args.gatewayToken ? "configured" : "not set"}`);
1280
+ if (args.transport === "sse") {
1281
+ const sseConfig = {
1282
+ port: args.port,
1283
+ host: args.host,
1284
+ issuerUrl: args.issuerUrl
1285
+ };
1286
+ if (args.authEnabled && args.clientId) {
1287
+ const clientIdRegex = /^[a-zA-Z0-9][a-zA-Z0-9_-]{2,63}$/;
1288
+ if (!clientIdRegex.test(args.clientId)) {
1289
+ logError(
1290
+ "MCP_CLIENT_ID is invalid. Must be 3-64 characters, alphanumeric/dashes/underscores, start with a letter or digit."
1291
+ );
1292
+ process.exit(1);
1293
+ }
1294
+ if (!args.clientSecret || args.clientSecret.length < 32) {
1295
+ logError(
1296
+ "MCP_CLIENT_SECRET must be at least 32 characters. Generate one with: openssl rand -hex 32"
1297
+ );
1298
+ process.exit(1);
1299
+ }
1300
+ sseConfig.authConfig = {
1301
+ clientId: args.clientId,
1302
+ clientSecret: args.clientSecret,
1303
+ redirectUris: args.redirectUris
1304
+ };
1305
+ log(`OAuth client ID: ${args.clientId}`);
1306
+ if (!args.redirectUris || args.redirectUris.length === 0) {
1307
+ log(
1308
+ "WARNING: MCP_REDIRECT_URIS not set \u2014 any redirect_uri will be accepted. Set MCP_REDIRECT_URIS for production."
1309
+ );
1310
+ }
1311
+ } else if (args.authEnabled && !args.clientId) {
1312
+ logError("AUTH_ENABLED=true but MCP_CLIENT_ID is not set. Refusing to start without auth.");
1313
+ process.exit(1);
1314
+ }
1315
+ await createSSEServer(sseConfig, deps);
1316
+ } else {
1317
+ const server = createMcpServer(deps);
1318
+ const transport = new StdioServerTransport();
1319
+ await server.connect(transport);
1320
+ log("OpenClaw MCP server running on stdio");
1321
+ }
1322
+ }
1323
+ main().catch((error) => {
1324
+ logError("Fatal error", error);
1325
+ process.exit(1);
1326
+ });