UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,432 lines 62.6 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, f as normalizeStringifiedOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { n as VERSION } from "./version-Crcn9X9T.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { At as boolean, Rn as string, Tn as object, dn as literal, wn as number, yt as _enum } from "./schemas-6cH6bZ7o.js";
import { r as extractFirstTextBlock } from "./chat-message-content-CnGjfPsN.js";
import { _ as buildMcpHttpFetch, g as runMcpOAuthLogin, h as readMcpOAuthCredentialsStatus, m as clearMcpOAuthCredentials, n as createSessionMcpRuntime, p as resolveMcpTransportConfig, v as withSameOriginMcpHttpHeaders, y as withoutMcpAuthorizationHeader } from "./agent-bundle-mcp-runtime-Bf36JLIh.js";
import { t as buildBundleMcpToolsFromCatalog } from "./agent-bundle-mcp-materialize-NJW_G1cx.js";
import { a as updateConfiguredMcpServerTools, i as updateConfiguredMcpServer, n as setConfiguredMcpServer, o as parseConfigValue, r as unsetConfiguredMcpServer, t as listConfiguredMcpServers } from "./mcp-config-DZdEMMwe.js";
import { t as resolveGatewayAuthOptions } from "./gateway-secret-options-CueD-vAi.js";
import { t as applyParentDefaultHelpAction } from "./parent-default-help-DQUF3qKA.js";
import { constants } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import "commander";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
//#region src/mcp/channel-shared.ts
/** Raw MCP notification schema emitted by Claude channel clients for permission prompts. */
const ClaudePermissionRequestSchema = object({
	method: literal("notifications/claude/channel/permission_request"),
	params: object({
		request_id: string(),
		tool_name: string(),
		description: string(),
		input_preview: string()
	})
});
/** Resolve the visible message id, including OpenClaw metadata attached to raw entries. */
function resolveMessageId(entry) {
	return normalizeOptionalString(entry.id) ?? (entry["__openclaw"] && typeof entry["__openclaw"] === "object" ? normalizeOptionalString(entry["__openclaw"].id) : void 0);
}
/** Build the text summary format expected by simple MCP tool results. */
function summarizeResult(label, count) {
	return { content: [{
		type: "text",
		text: `${label}: ${count}`
	}] };
}
/** Build a text summary plus pretty JSON payload for MCP clients without structured rendering. */
function summarizeStructuredResult(label, count, payload) {
	return { content: [{
		type: "text",
		text: `${label}: ${count}\n\n${JSON.stringify(payload, null, 2)}`
	}] };
}
function resolveConversationChannel(row) {
	return normalizeOptionalLowercaseString(normalizeOptionalString(row.deliveryContext?.channel) ?? normalizeOptionalString(row.lastChannel) ?? normalizeOptionalString(row.channel) ?? normalizeOptionalString(row.origin?.provider));
}
/** Convert a Gateway session row into a reply-capable conversation descriptor. */
function toConversation(row) {
	const channel = resolveConversationChannel(row);
	const to = normalizeOptionalString(row.deliveryContext?.to) ?? normalizeOptionalString(row.lastTo);
	if (!channel || !to) return null;
	return {
		sessionKey: row.key,
		channel,
		to,
		accountId: normalizeOptionalString(row.deliveryContext?.accountId) ?? normalizeOptionalString(row.lastAccountId) ?? normalizeOptionalString(row.origin?.accountId),
		threadId: row.deliveryContext?.threadId ?? row.lastThreadId ?? row.origin?.threadId,
		label: normalizeOptionalString(row.label),
		displayName: normalizeOptionalString(row.displayName),
		derivedTitle: normalizeOptionalString(row.derivedTitle),
		lastMessagePreview: normalizeOptionalString(row.lastMessagePreview),
		updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : null
	};
}
/** Check whether a queued event should be visible to a poll or wait call. */
function matchEventFilter(event, filter) {
	if (event.cursor <= filter.afterCursor) return false;
	if (!filter.sessionKey) return true;
	return "sessionKey" in event && event.sessionKey === filter.sessionKey;
}
/** Return non-text content blocks from a raw message payload. */
function extractAttachmentsFromMessage(message) {
	if (!message || typeof message !== "object") return [];
	const content = message.content;
	if (!Array.isArray(content)) return [];
	return content.filter((entry) => {
		if (!entry || typeof entry !== "object") return false;
		return normalizeOptionalString(entry.type) !== "text";
	});
}
/** Normalize approval identifiers before local tracking or resolution. */
function normalizeApprovalId(value) {
	const id = normalizeOptionalString(value);
	return id ? id.trim() : void 0;
}
//#endregion
//#region src/mcp/channel-bridge.ts
const CLAUDE_PERMISSION_REPLY_RE = /^(yes|no)\s+([a-km-z]{5})$/i;
const QUEUE_LIMIT = 1e3;
const PENDING_CLAUDE_PERMISSION_TTL_MS = 3600 * 1e3;
const PENDING_APPROVAL_DEFAULT_TTL_MS = 1800 * 1e3;
const PENDING_SWEEP_INTERVAL_MS = 300 * 1e3;
/** Connects the MCP server surface to a Gateway client and queues channel events for polling. */
var OpenClawChannelBridge = class {
	constructor(cfg, params) {
		this.cfg = cfg;
		this.params = params;
		this.gateway = null;
		this.queue = [];
		this.pendingWaiters = /* @__PURE__ */ new Set();
		this.pendingClaudePermissions = /* @__PURE__ */ new Map();
		this.pendingApprovals = /* @__PURE__ */ new Map();
		this.pendingSweepInterval = null;
		this.server = null;
		this.cursor = 0;
		this.closed = false;
		this.ready = false;
		this.started = false;
		this.retryingInitialConnect = false;
		this.readySettled = false;
		this.verbose = params.verbose;
		this.claudeChannelMode = params.claudeChannelMode;
		this.readyPromise = new Promise((resolve, reject) => {
			this.resolveReady = resolve;
			this.rejectReady = reject;
		});
	}
	/** Attach the MCP server used for outbound protocol notifications. */
	setServer(server) {
		this.server = server;
	}
	/** Start the Gateway connection and resolve only after session subscription succeeds. */
	async start() {
		if (this.started) {
			await this.readyPromise;
			return;
		}
		this.started = true;
		const [{ resolveGatewayClientBootstrap }, { GatewayClient: GatewayClientCtor }, { startGatewayClientWhenEventLoopReady }, { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE }, { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES }] = await Promise.all([
			import("./client-bootstrap-QvAdlzyz.js"),
			import("./client-hEKfHM6_.js"),
			import("./client-start-readiness-DooYwk6N.js"),
			import("./method-scopes-DGjyPA0d.js"),
			import("./client-info-9YxJlXWF.js")
		]);
		const bootstrap = await resolveGatewayClientBootstrap({
			config: this.cfg,
			gatewayUrl: this.params.gatewayUrl,
			explicitAuth: {
				token: this.params.gatewayToken,
				password: this.params.gatewayPassword
			},
			env: process.env
		});
		if (this.closed) {
			this.resolveReadyOnce();
			return;
		}
		this.gateway = new GatewayClientCtor({
			url: bootstrap.url,
			token: bootstrap.auth.token,
			password: bootstrap.auth.password,
			preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs,
			clientName: GATEWAY_CLIENT_NAMES.CLI,
			clientDisplayName: "OpenClaw MCP",
			clientVersion: VERSION,
			mode: GATEWAY_CLIENT_MODES.CLI,
			scopes: [
				READ_SCOPE,
				WRITE_SCOPE,
				APPROVALS_SCOPE
			],
			requestTimeoutMs: 18e4,
			onEvent: (event) => {
				this.handleGatewayEvent(event);
			},
			onHelloOk: () => {
				this.retryingInitialConnect = false;
				this.handleHelloOk();
			},
			onConnectError: (error) => {
				const normalizedError = error instanceof Error ? error : new Error(String(error));
				if (shouldRetryInitialMcpGatewayConnect(normalizedError)) {
					this.retryingInitialConnect = true;
					return;
				}
				this.rejectReadyOnce(normalizedError);
			},
			onClose: (code, reason) => {
				if (!this.ready && !this.closed && !this.retryingInitialConnect) this.rejectReadyOnce(/* @__PURE__ */ new Error(`gateway closed before ready (${code}): ${reason}`));
				this.retryingInitialConnect = false;
			}
		});
		if (!(await startGatewayClientWhenEventLoopReady(this.gateway, { clientOptions: { preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs } })).ready) this.rejectReadyOnce(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
		await this.readyPromise;
	}
	/** Wait until the bridge has subscribed to Gateway session events. */
	async waitUntilReady() {
		await this.readyPromise;
	}
	/** Stop Gateway IO and release waiters so MCP shutdown cannot hang on pending polls. */
	async close() {
		if (this.closed) return;
		this.closed = true;
		this.resolveReadyOnce();
		if (this.pendingSweepInterval) {
			clearInterval(this.pendingSweepInterval);
			this.pendingSweepInterval = null;
		}
		this.pendingClaudePermissions.clear();
		this.pendingApprovals.clear();
		for (const waiter of this.pendingWaiters) {
			if (waiter.timeout) clearTimeout(waiter.timeout);
			waiter.resolve(null);
		}
		this.pendingWaiters.clear();
		const gateway = this.gateway;
		this.gateway = null;
		await gateway?.stopAndWait().catch(() => void 0);
	}
	/** List Gateway sessions that have enough routing metadata to be channel conversations. */
	async listConversations(params) {
		await this.waitUntilReady();
		const response = await this.requestGateway("sessions.list", {
			limit: params?.limit ?? 50,
			search: params?.search,
			includeDerivedTitles: params?.includeDerivedTitles ?? true,
			includeLastMessage: params?.includeLastMessage ?? true
		});
		const requestedChannel = normalizeOptionalLowercaseString(params?.channel);
		return (response.sessions ?? []).map(toConversation).filter((conversation) => Boolean(conversation)).filter((conversation) => requestedChannel ? normalizeLowercaseStringOrEmpty(conversation.channel) === requestedChannel : true);
	}
	/** Resolve one conversation by its stable session key. */
	async getConversation(sessionKey) {
		const normalizedSessionKey = sessionKey.trim();
		if (!normalizedSessionKey) return null;
		await this.waitUntilReady();
		const response = await this.requestGateway("sessions.describe", {
			key: normalizedSessionKey,
			includeDerivedTitles: true,
			includeLastMessage: true
		});
		return response.session ? toConversation(response.session) : null;
	}
	/** Read recent history through the Gateway session API. */
	async readMessages(sessionKey, limit = 20) {
		await this.waitUntilReady();
		return (await this.requestGateway("sessions.get", {
			key: sessionKey,
			limit
		})).messages ?? [];
	}
	/** Send a reply using the same channel route stored on the conversation. */
	async sendMessage(params) {
		const conversation = await this.getConversation(params.sessionKey);
		if (!conversation) throw new Error(`Conversation not found for session ${params.sessionKey}`);
		return await this.requestGateway("send", {
			to: conversation.to,
			channel: conversation.channel,
			accountId: conversation.accountId,
			threadId: conversation.threadId == null ? void 0 : String(conversation.threadId),
			message: params.text,
			sessionKey: conversation.sessionKey,
			idempotencyKey: randomUUID()
		});
	}
	/** Return locally tracked approval requests that are still open. */
	listPendingApprovals() {
		this.sweepPendingExpired();
		return [...this.pendingApprovals.values()].map((entry) => entry.approval).toSorted((a, b) => {
			return (a.createdAtMs ?? 0) - (b.createdAtMs ?? 0);
		});
	}
	/** Forward an MCP approval decision to the matching Gateway approval resolver. */
	async respondToApproval(params) {
		if (params.kind === "exec") return await this.requestGateway("exec.approval.resolve", {
			id: params.id,
			decision: params.decision
		});
		return await this.requestGateway("plugin.approval.resolve", {
			id: params.id,
			decision: params.decision
		});
	}
	/** Poll queued events after a cursor without consuming them. */
	pollEvents(filter, limit = 20) {
		const events = this.queue.filter((event) => matchEventFilter(event, filter)).slice(0, limit);
		return {
			events,
			nextCursor: events.at(-1)?.cursor ?? filter.afterCursor
		};
	}
	/** Wait for the next matching event, resolving null on timeout or bridge close. */
	async waitForEvent(filter, timeoutMs = 3e4) {
		const existing = this.queue.find((event) => matchEventFilter(event, filter));
		if (existing) return existing;
		return await new Promise((resolve) => {
			const waiter = {
				filter,
				resolve: (value) => {
					this.pendingWaiters.delete(waiter);
					resolve(value);
				},
				timeout: null
			};
			if (timeoutMs > 0) waiter.timeout = setTimeout(() => {
				waiter.resolve(null);
			}, timeoutMs);
			this.pendingWaiters.add(waiter);
		});
	}
	/** Accept a Claude channel permission notification and expose it through event polling. */
	async handleClaudePermissionRequest(params) {
		if (this.closed) return;
		this.pendingClaudePermissions.set(params.requestId, Date.now());
		this.ensurePendingSweeper();
		this.enqueue({
			cursor: this.nextCursor(),
			type: "claude_permission_request",
			requestId: params.requestId,
			toolName: params.toolName,
			description: params.description,
			inputPreview: params.inputPreview
		});
		if (this.verbose) process.stderr.write(`openclaw mcp: pending Claude permission ${params.requestId}\n`);
	}
	async requestGateway(method, params) {
		if (!this.gateway) throw new Error("Gateway client is not ready");
		return await this.gateway.request(method, params);
	}
	async sendNotification(notification) {
		if (!this.server || this.closed) return;
		try {
			await this.server.server.notification(notification);
		} catch (error) {
			if (this.closed) return;
			process.stderr.write(`openclaw mcp: notification ${notification.method} failed\n`);
			if (this.verbose) process.stderr.write(`openclaw mcp: notification ${notification.method} error: ${String(error)}\n`);
		}
	}
	async handleHelloOk() {
		try {
			await this.requestGateway("sessions.subscribe", {});
			this.ready = true;
			this.resolveReadyOnce();
		} catch (error) {
			this.rejectReadyOnce(error instanceof Error ? error : new Error(String(error)));
		}
	}
	resolveReadyOnce() {
		if (this.readySettled) return;
		this.readySettled = true;
		this.resolveReady();
	}
	rejectReadyOnce(error) {
		if (this.readySettled) return;
		this.readySettled = true;
		this.rejectReady(error);
	}
	nextCursor() {
		this.cursor += 1;
		return this.cursor;
	}
	enqueue(event) {
		this.queue.push(event);
		while (this.queue.length > QUEUE_LIMIT) this.queue.shift();
		for (const waiter of this.pendingWaiters) {
			if (!matchEventFilter(event, waiter.filter)) continue;
			if (waiter.timeout) clearTimeout(waiter.timeout);
			waiter.resolve(event);
		}
	}
	trackApproval(kind, payload) {
		if (this.closed) return;
		const id = normalizeApprovalId(payload.id);
		if (!id) return;
		this.pendingApprovals.set(id, {
			approval: {
				kind,
				id,
				request: payload.request && typeof payload.request === "object" ? payload.request : void 0,
				createdAtMs: typeof payload.createdAtMs === "number" ? payload.createdAtMs : void 0,
				expiresAtMs: typeof payload.expiresAtMs === "number" ? payload.expiresAtMs : void 0
			},
			trackedAtMs: Date.now()
		});
		this.ensurePendingSweeper();
	}
	ensurePendingSweeper() {
		if (this.pendingSweepInterval || this.closed) return;
		this.pendingSweepInterval = setInterval(() => {
			this.sweepPendingExpired();
		}, PENDING_SWEEP_INTERVAL_MS);
		this.pendingSweepInterval.unref();
	}
	sweepPendingExpired(now = Date.now()) {
		for (const [id, createdAtMs] of this.pendingClaudePermissions) if (now - createdAtMs >= PENDING_CLAUDE_PERMISSION_TTL_MS) this.pendingClaudePermissions.delete(id);
		for (const [id, entry] of this.pendingApprovals) if (now >= (entry.approval.expiresAtMs ?? entry.trackedAtMs + PENDING_APPROVAL_DEFAULT_TTL_MS)) this.pendingApprovals.delete(id);
		if (this.pendingSweepInterval && this.pendingClaudePermissions.size === 0 && this.pendingApprovals.size === 0) {
			clearInterval(this.pendingSweepInterval);
			this.pendingSweepInterval = null;
		}
	}
	resolveTrackedApproval(payload) {
		const id = normalizeApprovalId(payload.id);
		if (id) this.pendingApprovals.delete(id);
	}
	async handleGatewayEvent(event) {
		switch (event.event) {
			case "session.message":
				await this.handleSessionMessageEvent(event.payload);
				return;
			case "exec.approval.requested": {
				const raw = event.payload ?? {};
				this.trackApproval("exec", raw);
				this.enqueue({
					cursor: this.nextCursor(),
					type: "exec_approval_requested",
					raw
				});
				return;
			}
			case "exec.approval.resolved": {
				const raw = event.payload ?? {};
				this.resolveTrackedApproval(raw);
				this.enqueue({
					cursor: this.nextCursor(),
					type: "exec_approval_resolved",
					raw
				});
				return;
			}
			case "plugin.approval.requested": {
				const raw = event.payload ?? {};
				this.trackApproval("plugin", raw);
				this.enqueue({
					cursor: this.nextCursor(),
					type: "plugin_approval_requested",
					raw
				});
				return;
			}
			case "plugin.approval.resolved": {
				const raw = event.payload ?? {};
				this.resolveTrackedApproval(raw);
				this.enqueue({
					cursor: this.nextCursor(),
					type: "plugin_approval_resolved",
					raw
				});
			}
		}
	}
	async handleSessionMessageEvent(payload) {
		const sessionKey = normalizeOptionalString(payload.sessionKey);
		if (!sessionKey) return;
		const conversation = toConversation({
			key: sessionKey,
			lastChannel: normalizeOptionalString(payload.lastChannel),
			lastTo: normalizeOptionalString(payload.lastTo),
			lastAccountId: normalizeOptionalString(payload.lastAccountId),
			lastThreadId: payload.lastThreadId
		}) ?? void 0;
		const role = normalizeOptionalString(payload.message?.role);
		const text = extractFirstTextBlock(payload.message);
		const permissionMatch = text ? CLAUDE_PERMISSION_REPLY_RE.exec(text) : null;
		if (permissionMatch) {
			const requestId = normalizeOptionalLowercaseString(permissionMatch[2]);
			if (requestId && this.pendingClaudePermissions.has(requestId)) {
				this.pendingClaudePermissions.delete(requestId);
				await this.sendNotification({
					method: "notifications/claude/channel/permission",
					params: {
						request_id: requestId,
						behavior: normalizeLowercaseStringOrEmpty(permissionMatch[1]).startsWith("y") ? "allow" : "deny"
					}
				});
				return;
			}
		}
		this.enqueue({
			cursor: this.nextCursor(),
			type: "message",
			sessionKey,
			conversation,
			messageId: normalizeOptionalString(payload.messageId),
			messageSeq: typeof payload.messageSeq === "number" ? payload.messageSeq : void 0,
			role,
			text,
			raw: payload
		});
		if (!this.shouldEmitClaudeChannel(role, conversation)) return;
		await this.sendNotification({
			method: "notifications/claude/channel",
			params: {
				content: text ?? "[non-text message]",
				meta: {
					session_key: sessionKey,
					channel: conversation?.channel ?? "",
					to: conversation?.to ?? "",
					account_id: conversation?.accountId ?? "",
					thread_id: conversation?.threadId == null ? "" : String(conversation.threadId),
					message_id: normalizeOptionalString(payload.messageId) ?? ""
				}
			}
		});
	}
	shouldEmitClaudeChannel(role, conversation) {
		if (this.claudeChannelMode === "off") return false;
		if (role !== "user") return false;
		return Boolean(conversation);
	}
};
/** Decide whether startup should wait for a retryable Gateway connect failure to recover. */
function shouldRetryInitialMcpGatewayConnect(error) {
	if (error.name === "GatewayClientRequestError" && "retryable" in error && typeof error.retryable === "boolean") return error.retryable;
	const message = error.message.toLowerCase();
	return message.includes("gateway request timeout for connect") || message.includes("gateway connect challenge timeout");
}
//#endregion
//#region src/mcp/channel-tools.ts
/**
* MCP tool registration for channel conversation access.
*
* Tool handlers stay thin: schemas validate public inputs and the bridge owns
* Gateway readiness, routing, event queueing, and approval resolution.
*/
/** Return protocol capabilities advertised when Claude channel mode is enabled. */
function getChannelMcpCapabilities(claudeChannelMode) {
	if (claudeChannelMode === "off") return;
	return { experimental: {
		"claude/channel": {},
		"claude/channel/permission": {}
	} };
}
/** Register all channel MCP tools against a server instance. */
function registerChannelMcpTools(server, bridge) {
	server.tool("conversations_list", "List OpenClaw channel-backed conversations available through session routes.", {
		limit: number().int().min(1).max(500).optional(),
		search: string().optional(),
		channel: string().optional(),
		includeDerivedTitles: boolean().optional(),
		includeLastMessage: boolean().optional()
	}, async (args) => {
		const conversations = await bridge.listConversations(args);
		return {
			...summarizeStructuredResult("conversations", conversations.length, { conversations }),
			structuredContent: { conversations }
		};
	});
	server.tool("conversation_get", "Get one OpenClaw conversation by session key.", { session_key: string().min(1) }, async ({ session_key }) => {
		const conversation = await bridge.getConversation(session_key);
		if (!conversation) return {
			content: [{
				type: "text",
				text: `conversation not found: ${session_key}`
			}],
			isError: true
		};
		return {
			content: [{
				type: "text",
				text: `conversation ${conversation.sessionKey}`
			}],
			structuredContent: { conversation }
		};
	});
	server.tool("messages_read", "Read recent messages for one OpenClaw conversation.", {
		session_key: string().min(1),
		limit: number().int().min(1).max(200).optional()
	}, async ({ session_key, limit }) => {
		const messages = await bridge.readMessages(session_key, limit ?? 20);
		return {
			...summarizeStructuredResult("messages", messages.length, { messages }),
			structuredContent: { messages }
		};
	});
	server.tool("attachments_fetch", "List non-text attachments for a message in one OpenClaw conversation.", {
		session_key: string().min(1),
		message_id: string().min(1),
		limit: number().int().min(1).max(200).optional()
	}, async ({ session_key, message_id, limit }) => {
		const message = (await bridge.readMessages(session_key, limit ?? 100)).find((entry) => resolveMessageId(entry) === message_id);
		if (!message) return {
			content: [{
				type: "text",
				text: `message not found: ${message_id}`
			}],
			isError: true
		};
		const attachments = extractAttachmentsFromMessage(message);
		return {
			...summarizeResult("attachments", attachments.length),
			structuredContent: {
				attachments,
				message
			}
		};
	});
	server.tool("events_poll", "Poll queued OpenClaw conversation events since a cursor.", {
		after_cursor: number().int().min(0).optional(),
		session_key: string().optional(),
		limit: number().int().min(1).max(200).optional()
	}, async ({ after_cursor, session_key, limit }) => {
		const { events, nextCursor } = bridge.pollEvents({
			afterCursor: after_cursor ?? 0,
			sessionKey: normalizeOptionalString(session_key)
		}, limit ?? 20);
		return {
			...summarizeResult("events", events.length),
			structuredContent: {
				events,
				next_cursor: nextCursor
			}
		};
	});
	server.tool("events_wait", "Wait for the next queued OpenClaw conversation event.", {
		after_cursor: number().int().min(0).optional(),
		session_key: string().optional(),
		timeout_ms: number().int().min(1).max(3e5).optional()
	}, async ({ after_cursor, session_key, timeout_ms }) => {
		const event = await bridge.waitForEvent({
			afterCursor: after_cursor ?? 0,
			sessionKey: normalizeOptionalString(session_key)
		}, timeout_ms ?? 3e4);
		return {
			content: [{
				type: "text",
				text: event ? `event ${event.cursor}` : "timeout"
			}],
			structuredContent: { event }
		};
	});
	server.tool("messages_send", "Send a message back through the same OpenClaw conversation route.", {
		session_key: string().min(1),
		text: string().min(1)
	}, async ({ session_key, text }) => {
		return {
			content: [{
				type: "text",
				text: "sent"
			}],
			structuredContent: { result: await bridge.sendMessage({
				sessionKey: session_key,
				text
			}) }
		};
	});
	server.tool("permissions_list_open", "List open OpenClaw exec or plugin approval requests visible through the Gateway.", {}, async () => {
		const approvals = bridge.listPendingApprovals();
		return {
			...summarizeResult("approvals", approvals.length),
			structuredContent: { approvals }
		};
	});
	server.tool("permissions_respond", "Allow or deny one pending OpenClaw exec or plugin approval request.", {
		kind: _enum(["exec", "plugin"]),
		id: string().min(1),
		decision: _enum([
			"allow-once",
			"allow-always",
			"deny"
		])
	}, async ({ kind, id, decision }) => {
		return {
			content: [{
				type: "text",
				text: "approval resolved"
			}],
			structuredContent: { result: await bridge.respondToApproval({
				kind,
				id,
				decision
			}) }
		};
	});
}
//#endregion
//#region src/mcp/channel-server.ts
async function resolveMcpConfig(config) {
	if (config) return config;
	const { getRuntimeConfig } = await import("./config/config.js");
	return getRuntimeConfig();
}
/** Create an in-process channel MCP server plus explicit start and close hooks. */
async function createOpenClawChannelMcpServer(opts = {}) {
	const cfg = await resolveMcpConfig(opts.config);
	const claudeChannelMode = opts.claudeChannelMode ?? "auto";
	const capabilities = getChannelMcpCapabilities(claudeChannelMode);
	const server = new McpServer({
		name: "openclaw",
		version: VERSION
	}, capabilities ? { capabilities } : void 0);
	const bridge = new OpenClawChannelBridge(cfg, {
		gatewayUrl: opts.gatewayUrl,
		gatewayToken: opts.gatewayToken,
		gatewayPassword: opts.gatewayPassword,
		claudeChannelMode,
		verbose: opts.verbose ?? false
	});
	bridge.setServer(server);
	server.server.setNotificationHandler(ClaudePermissionRequestSchema, async ({ params }) => {
		await bridge.handleClaudePermissionRequest({
			requestId: params.request_id,
			toolName: params.tool_name,
			description: params.description,
			inputPreview: params.input_preview
		});
	});
	registerChannelMcpTools(server, bridge);
	return {
		server,
		bridge,
		start: async () => {
			await bridge.start();
		},
		close: async () => {
			await bridge.close();
			await server.close();
		}
	};
}
/** Serve the channel MCP server over stdio until transport or process shutdown. */
async function serveOpenClawChannelMcp(opts = {}) {
	const { server, start, close } = await createOpenClawChannelMcpServer(opts);
	const transport = new StdioServerTransport();
	let shuttingDown = false;
	let resolveClosed;
	const closed = new Promise((resolve) => {
		resolveClosed = resolve;
	});
	const shutdown = () => {
		if (shuttingDown) return;
		shuttingDown = true;
		process.stdin.off("end", shutdown);
		process.stdin.off("close", shutdown);
		process.off("SIGINT", shutdown);
		process.off("SIGTERM", shutdown);
		transport["onclose"] = void 0;
		close().then(resolveClosed, resolveClosed);
	};
	transport["onclose"] = shutdown;
	process.stdin.once("end", shutdown);
	process.stdin.once("close", shutdown);
	process.once("SIGINT", shutdown);
	process.once("SIGTERM", shutdown);
	try {
		await server.connect(transport);
		await start();
		await closed;
	} finally {
		shutdown();
		await closed;
	}
}
//#endregion
//#region src/cli/mcp-cli.ts
function fail(message) {
	defaultRuntime.error(message);
	defaultRuntime.exit(1);
	throw new Error(message);
}
function printJson(value) {
	defaultRuntime.writeJson(value);
}
function parseCsvList(value) {
	if (!value) return;
	const entries = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
	return entries.length > 0 ? entries : void 0;
}
function collectOption(value, previous = []) {
	return [...previous, value];
}
function parseKeyValueEntries(values, label) {
	const entries = {};
	for (const raw of values ?? []) {
		const separatorIndex = raw.indexOf("=");
		if (separatorIndex <= 0) fail(`${label} entries must use KEY=VALUE.`);
		const key = raw.slice(0, separatorIndex).trim();
		const value = raw.slice(separatorIndex + 1);
		if (!key) fail(`${label} entries must use a non-empty key.`);
		entries[key] = value;
	}
	return Object.keys(entries).length > 0 ? entries : void 0;
}
function parsePositiveNumberOption(value, label) {
	if (value === void 0) return;
	const parsed = Number(value);
	if (!Number.isFinite(parsed) || parsed <= 0) fail(`${label} must be a positive number.`);
	return parsed;
}
function parseOAuthConfig(opts) {
	const oauth = {
		...normalizeStringifiedOptionalString(opts.scope) ? { scope: opts.scope.trim() } : {},
		...normalizeStringifiedOptionalString(opts.redirectUrl) ? { redirectUrl: opts.redirectUrl.trim() } : {},
		...normalizeStringifiedOptionalString(opts.clientMetadataUrl) ? { clientMetadataUrl: opts.clientMetadataUrl.trim() } : {}
	};
	return Object.keys(oauth).length > 0 ? oauth : void 0;
}
async function clearMcpOAuthCredentialsForConfiguredServer(name, server) {
	const resolved = resolveMcpTransportConfig(name, server);
	if (resolved?.kind === "http") await clearMcpOAuthCredentials({
		serverName: name,
		serverUrl: resolved.url
	});
}
function hasOAuthAuth(server) {
	return typeof server === "object" && server !== null && "auth" in server && server.auth === "oauth";
}
async function clearStaleMcpOAuthCredentialsForReplacement(params) {
	if (!hasOAuthAuth(params.previous)) return;
	const previousResolved = resolveMcpTransportConfig(params.name, params.previous);
	if (previousResolved?.kind !== "http") return;
	const nextResolved = hasOAuthAuth(params.next) ? resolveMcpTransportConfig(params.name, params.next) : void 0;
	if (nextResolved?.kind === "http" && nextResolved.url === previousResolved.url) return;
	await clearMcpOAuthCredentials({
		serverName: params.name,
		serverUrl: previousResolved.url
	});
}
function setOptionalField(target, key, value) {
	if (value !== void 0) target[key] = value;
}
const SENSITIVE_HEADER_NAMES = new Set([
	"authorization",
	"proxy-authorization",
	"x-api-key",
	"api-key",
	"api_key"
]);
const SENSITIVE_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|authorization|bearer|password|secret|token)$/i;
function asRecord(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
}
function issue(level, message) {
	return {
		level,
		message
	};
}
function hasSensitiveKey(name) {
	return SENSITIVE_HEADER_NAMES.has(name.trim().toLowerCase()) || SENSITIVE_KEY_PATTERN.test(name);
}
function hasLiteralSensitiveValue(value) {
	return typeof value === "string" && value.trim().length > 0 && !value.trim().startsWith("$");
}
function resolveConfiguredPath(filePath, cwd) {
	if (path.isAbsolute(filePath)) return filePath;
	const base = typeof cwd === "string" && cwd.trim() ? cwd.trim() : process.cwd();
	return path.resolve(base, filePath);
}
async function fileExists(filePath) {
	try {
		return (await fs$1.stat(filePath)).isFile();
	} catch {
		return false;
	}
}
async function directoryExists(filePath) {
	try {
		return (await fs$1.stat(filePath)).isDirectory();
	} catch {
		return false;
	}
}
async function isExecutable(filePath) {
	try {
		await fs$1.access(filePath, process.platform === "win32" ? constants.F_OK : constants.X_OK);
		return true;
	} catch {
		return false;
	}
}
function executableCandidates(command) {
	if (process.platform !== "win32") return [command];
	const extensions = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((entry) => entry.trim()).filter(Boolean);
	if (path.extname(command)) return [command];
	return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`)];
}
function resolveEffectivePath(env) {
	if (!env) return process.env.PATH ?? "";
	if (typeof env.PATH === "string") return env.PATH;
	if (process.platform === "win32" && typeof env.Path === "string") return env.Path;
	return process.env.PATH ?? "";
}
async function commandExists(command, cwd, env) {
	if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) return isExecutable(resolveConfiguredPath(command, cwd));
	const pathEntries = resolveEffectivePath(env).split(path.delimiter).map((entry) => entry.trim() || ".");
	for (const pathEntry of pathEntries) {
		const resolvedPathEntry = path.isAbsolute(pathEntry) ? pathEntry : resolveConfiguredPath(pathEntry, cwd);
		for (const candidate of executableCandidates(command)) if (await isExecutable(path.join(resolvedPathEntry, candidate))) return true;
	}
	return false;
}
async function collectMcpDoctorIssues(params) {
	const issues = [];
	const { name, server } = params;
	const resolved = resolveMcpTransportConfig(name, server);
	const disabled = server.enabled === false;
	if (server.enabled === false) issues.push(issue("warning", "server is disabled"));
	if (!disabled) {
		if (!resolved) issues.push(issue("error", "server transport is invalid"));
		if (resolved?.kind === "stdio") {
			if (!await commandExists(resolved.command, resolved.cwd, resolved.env)) issues.push(issue("error", `stdio command not found or not executable: ${resolved.command}`));
			if (resolved.cwd && !await directoryExists(resolved.cwd)) issues.push(issue("error", `stdio cwd does not exist: ${resolved.cwd}`));
		}
		if (resolved?.kind === "http") {
			if (server.auth === "oauth") {
				if (!(await readMcpOAuthCredentialsStatus({
					serverName: name,
					serverUrl: resolved.url
				})).hasTokens) issues.push(issue("warning", `OAuth credentials are not authorized; run ${formatCliCommand(`openclaw mcp login ${name}`)}`));
				const headers = asRecord(server.headers);
				if (headers && "Authorization" in headers) issues.push(issue("warning", "OAuth is enabled and the static Authorization header is ignored"));
			}
			if (resolved.sslVerify === false) issues.push(issue("warning", "TLS certificate verification is disabled"));
			if (resolved.clientCert && !await fileExists(resolveConfiguredPath(resolved.clientCert, ""))) issues.push(issue("error", `client certificate file does not exist: ${resolved.clientCert}`));
			if (resolved.clientKey && !await fileExists(resolveConfiguredPath(resolved.clientKey, ""))) issues.push(issue("error", `client key file does not exist: ${resolved.clientKey}`));
		}
	}
	for (const [field, values] of [["headers", asRecord(server.headers)], ["env", asRecord(server.env)]]) for (const [key, value] of Object.entries(values ?? {})) if (hasSensitiveKey(key) && hasLiteralSensitiveValue(value)) issues.push(issue("warning", `${field}.${key} contains a literal sensitive value; prefer an environment-backed value outside committed config`));
	const toolFilter = asRecord(server.toolFilter);
	if (toolFilter && !Array.isArray(toolFilter.include) && !Array.isArray(toolFilter.exclude)) issues.push(issue("warning", "toolFilter is present but has no include or exclude list"));
	if (params.probe && server.enabled !== false && !issues.some((entry) => entry.level === "error")) {
		const probeIssue = await probeMcpServerIssue({
			config: params.config,
			name,
			server
		});
		if (probeIssue) issues.push(probeIssue);
	}
	return issues;
}
async function probeMcpServerIssue(params) {
	const runtime = createSessionMcpRuntime({
		sessionId: "openclaw-cli-mcp-doctor",
		workspaceDir: process.cwd(),
		cfg: buildMcpProbeConfig({
			config: params.config,
			servers: { [params.name]: params.server }
		}),
		manifestRegistry: { plugins: [] }
	});
	try {
		const result = formatMcpProbeResult(await runtime.getCatalog());
		const diagnostic = result.diagnostics[0];
		if (diagnostic) return issue("error", `probe failed: ${diagnostic.message}`);
		if (!result.servers[params.name]) return issue("error", "probe did not connect to this server");
		return null;
	} catch (err) {
		return issue("error", `probe failed: ${formatErrorMessage(err)}`);
	} finally {
		await runtime.dispose();
	}
}
async function buildMcpStatusEntries(servers) {
	const entries = Object.entries(servers).toSorted(([a], [b]) => a.localeCompare(b));
	return Promise.all(entries.map(async ([name, server]) => {
		const resolved = resolveMcpTransportConfig(name, server);
		const enabled = server.enabled !== false;
		const entry = {
			name,
			configured: true,
			enabled,
			ok: enabled && Boolean(resolved),
			transport: resolved?.transportType,
			launch: resolved?.description,
			requestTimeoutMs: resolved?.requestTimeoutMs,
			connectionTimeoutMs: resolved?.connectionTimeoutMs,
			supportsParallelToolCalls: resolved?.supportsParallelToolCalls,
			toolFilter: server.toolFilter,
			codex: server.codex
		};
		if (server.auth) entry.auth = server.auth;
		if (server.auth === "oauth" && resolved?.kind === "http") entry.authStatus = await readMcpOAuthCredentialsStatus({
			serverName: name,
			serverUrl: resolved.url
		});
		return entry;
	}));
}
function formatMcpProbeResult(catalog) {
	const projectedTools = buildBundleMcpToolsFromCatalog({
		catalog,
		createResourceListExecute: () => async () => {
			throw new Error("probe projection cannot execute MCP resources_list");
		},
		createResourceReadExecute: () => async () => {
			throw new Error("probe projection cannot execute MCP resources_read");
		},
		createPromptListExecute: () => async () => {
			throw new Error("probe projection cannot execute MCP prompts_list");
		},
		createPromptGetExecute: () => async () => {
			throw new Error("probe projection cannot execute MCP prompts_get");
		}
	});
	return {
		generatedAt: new Date(catalog.generatedAt).toISOString(),
		servers: Object.fromEntries(Object.entries(catalog.servers).toSorted(([a], [b]) => a.localeCompare(b)).map(([name, server]) => [name, {
			launch: server.launchSummary,
			tools: server.toolCount,
			...server.requestTimeoutMs ? { requestTimeoutMs: server.requestTimeoutMs } : {},
			...server.supportsParallelToolCalls ? { supportsParallelToolCalls: server.supportsParallelToolCalls } : {},
			...server.tools?.filteredCount ? { filteredTools: server.tools.filteredCount } : {},
			...server.resources ? { resources: true } : {},
			...server.prompts ? { prompts: true } : {},
			...server.tools?.listChanged || server.resources?.listChanged || server.prompts?.listChanged ? { listChanged: {
				tools: server.tools?.listChanged === true,
				resources: server.resources?.listChanged === true,
				prompts: server.prompts?.listChanged === true
			} } : {}
		}])),
		tools: projectedTools.map((tool) => tool.name).toSorted(),
		diagnostics: catalog.diagnostics ?? []
	};
}
function buildMcpProbeConfig(params) {
	return {
		...params.config,
		mcp: {
			...params.config.mcp,
			servers: params.servers
		}
	};
}
async function probeMcpServersOrFail(params) {
	const runtime = createSessionMcpRuntime({
		sessionId: "openclaw-cli-mcp-probe",
		workspaceDir: process.cwd(),
		cfg: buildMcpProbeConfig({
			config: params.config,
			servers: params.servers
		}),
		manifestRegistry: { plugins: [] }
	});
	try {
		const result = formatMcpProbeResult(await runtime.getCatalog());
		if (result.diagnostics.length > 0) {
			const first = result.diagnostics[0];
			fail(`MCP probe failed for "${first.serverName}" in ${params.path}: ${first.message}`);
		}
		for (const name of Object.keys(params.servers)) if (!result.servers[name]) fail(`MCP probe did not connect to "${name}" in ${params.path}.`);
		return result;
	} finally {
		await runtime.dispose();
	}
}
function registerMcpCli(program) {
	const mcp = program.command("mcp").description("Manage OpenClaw MCP config and channel bridge");
	mcp.command("serve").description("Expose OpenClaw channels over MCP stdio").option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)").option("--token <token>", "Gateway token (if required)").option("--token-file <path>", "Read gateway token from file").option("--password <password>", "Gateway password (if required)").option("--password-file <path>", "Read gateway password from file").option("--claude-channel-mode <mode>", "Claude channel notification mode: auto, on, or off", "auto").option("-v, --verbose", "Verbose logging to stderr", false).action(async (opts) => {
		try {
			const { gatewayToken, gatewayPassword } = resolveGatewayAuthOptions(opts);
			const claudeChannelMode = normalizeLowercaseStringOrEmpty(normalizeStringifiedOptionalString(opts.claudeChannelMode) ?? "auto");
			if (claudeChannelMode !== "auto" && claudeChannelMode !== "on" && claudeChannelMode !== "off") throw new Error("Invalid --claude-channel-mode value. Use \"auto\", \"on\", or \"off\".");
			await serveOpenClawChannelMcp({
				gatewayUrl: opts.url,
				gatewayToken,
				gatewayPassword,
				claudeChannelMode,
				verbose: Boolean(opts.verbose)
			});
		} catch (err) {
			defaultRuntime.error(`MCP server failed to start: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw mcp list")} to inspect configured servers.`);
			defaultRuntime.exit(1);
		}
	});
	mcp.command("list").description("List configured MCP servers").option("--json", "Print JSON").action(async (opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		if (opts.json) {
			printJson(loaded.mcpServers);
			return;
		}
		const names = Object.keys(loaded.mcpServers).toSorted();
		if (names.length === 0) {
			defaultRuntime.log(`No MCP servers configured in ${loaded.path}. Add one with ${formatCliCommand("openclaw mcp set <name> '{\"command\":\"uvx\",\"args\":[\"context7-mcp\"]}'")}.`);
			return;
		}
		defaultRuntime.log(`MCP servers (${loaded.path}):`);
		for (const name of names) defaultRuntime.log(`- ${name}`);
	});
	mcp.command("show").description("Show one configured MCP server or the full MCP config").argument("[name]", "MCP server name").option("--json", "Print JSON").action(async (name, opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const value = name ? loaded.mcpServers[name] : loaded.mcpServers;
		if (name && !value) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		if (opts.json) {
			printJson(value ?? {});
			return;
		}
		if (name) defaultRuntime.log(`MCP server "${name}" (${loaded.path}):`);
		else defaultRuntime.log(`MCP servers (${loaded.path}):`);
		printJson(value ?? {});
	});
	mcp.command("status").description("Show configured MCP server transport status without connecting").option("-v, --verbose", "Show transport, auth, timeout, and filter details", false).option("--json", "Print JSON").action(async (opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const status = await buildMcpStatusEntries(loaded.mcpServers);
		if (opts.json) {
			printJson({
				path: loaded.path,
				servers: status
			});
			return;
		}
		if (status.length === 0) {
			defaultRuntime.log(`No MCP servers configured in ${loaded.path}.`);
			return;
		}
		defaultRuntime.log(`MCP server status (${loaded.path}):`);
		for (const entry of status) {
			const transport = entry.enabled ? entry.transport ?? "invalid" : "disabled";
			const auth = entry.auth === "oauth" ? " oauth" : "";
			const oauth = entry.authStatus?.hasTokens ? " authorized" : "";
			const filters = entry.toolFilter ? " tool-filtered" : "";
			const parallel = entry.supportsParallelToolCalls ? " parallel" : "";
			defaultRuntime.log(`- ${entry.name}: ${transport}${auth}${oauth}${filters}${parallel}`);
			if (opts.verbose) {
				defaultRuntime.log(`  launch: ${entry.launch ?? "n/a"}`);
				defaultRuntime.log(`  timeouts: connect=${entry.connectionTimeoutMs ?? "n/a"}ms request=${entry.requestTimeoutMs ?? "n/a"}ms`);
				if (entry.auth === "oauth") defaultRuntime.log(`  oauth: tokens=${entry.authStatus?.hasTokens ? "yes" : "no"} client=${entry.authStatus?.hasClientInformation ? "yes" : "no"}`);
				if (entry.toolFilter) defaultRuntime.log(`  tools: ${JSON.stringify(entry.toolFilter)}`);
			}
		}
	});
	mcp.command("probe").description("Connect to configured MCP servers and list available capabilities").argument("[name]", "MCP server name").option("--json", "Print JSON").action(async (name, opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const servers = name ? loaded.mcpServers[name] ? { [name]: loaded.mcpServers[name] } : void 0 : loaded.mcpServers;
		if (!servers) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		if (name && loaded.mcpServers[name]?.enabled === false) fail(`MCP server "${name}" is disabled in ${loaded.path}. Run ${formatCliCommand(`openclaw mcp configure ${name} --enable`)} before probing it.`);
		const runtime = createSessionMcpRuntime({
			sessionId: "openclaw-cli-mcp-probe",
			workspaceDir: process.cwd(),
			cfg: buildMcpProbeConfig({
				config: loaded.config,
				servers
			}),
			manifestRegistry: { plugins: [] }
		});
		try {
			const result = formatMcpProbeResult(await runtime.getCatalog());
			if (opts.json) {
				printJson(result);
				return;
			}
			defaultRuntime.log(`MCP probe (${loaded.path}):`);
			for (const [serverName, server] of Object.entries(result.servers)) defaultRuntime.log(`- ${serverName}: ${server.tools} tools${server.resources ? ", resources" : ""}${server.prompts ? ", prompts" : ""}`);
			for (const diagnostic of result.diagnostics) defaultRuntime.log(`! ${diagnostic.serverName}: ${diagnostic.message}`);
		} finally {
			await runtime.dispose();
		}
	});
	mcp.command("doctor").description("Check configured MCP servers for static setup problems").argument("[name]", "MCP server name").option("--probe", "Also connect to each checked server", false).option("--json", "Print JSON").action(async (name, opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const selected = name ? loaded.mcpServers[name] ? { [name]: loaded.mcpServers[name] } : void 0 : loaded.mcpServers;
		if (!selected) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		const servers = await Promise.all(Object.entries(selected).toSorted(([a], [b]) => a.localeCompare(b)).map(async ([serverName, server]) => {
			const issues = await collectMcpDoctorIssues({
				name: serverName,
				server,
				config: loaded.config,
				path: loaded.path,
				probe: Boolean(opts.probe)
			});
			return {
				name: serverName,
				ok: !issues.some((entry) => entry.level === "error"),
				issues
			};
		}));
		const ok = servers.every((server) => server.ok);
		if (opts.json) {
			printJson({
				path: loaded.path,
				ok,
				servers
			});
			if (!ok) fail("MCP doctor found errors.");
			return;
		}
		if (servers.length === 0) {
			defaultRuntime.log(`No MCP servers configured in ${loaded.path}. Add one with ${formatCliCommand("openclaw mcp add <name> --command <command>")}.`);
			return;
		}
		defaultRuntime.log(`MCP doctor (${loaded.path}):`);
		for (const server of servers) {
			defaultRuntime.log(`- ${server.name}: ${server.ok ? "ok" : "issues"}`);
			for (const entry of server.issues) {
				const prefix = entry.level === "error" ? "!" : entry.level === "warning" ? "-" : "i";
				defaultRuntime.log(`  ${prefix} ${entry.level}: ${entry.message}`);
			}
		}
		if (!ok) fail("MCP doctor found errors.");
	});
	mcp.command("add").description("Add one MCP server from flags and probe it before saving").argument("<name>", "MCP server name").option("--command <command>", "Stdio command to spawn").option("--arg <value>", "Repeatable stdio argument", collectOption, []).option("--env <key=value>", "Repeatable stdio environment entry", collectOption, []).option("--cwd <path>", "Working directory for stdio server").option("--url <url>", "HTTP MCP server URL").option("--transport <type>", "HTTP transport: streamable-http or sse").option("--header <key=value>", "Repeatable HTTP header", collectOption, []).option("--auth <mode>", "HTTP auth mode: oauth").option("--oauth-scope <scope>", "OAuth scope").option("--oauth-redirect-url <url>", "OAuth redirect URL").option("--oauth-client-metadata-url <url>", "OAuth client metadata URL").option("--include <csv>", "Comma-separated MCP tool names or '*' globs to expose").option("--exclude <csv>", "Comma-separated MCP tool names or '*' globs to hide").option("--timeout <seconds>", "Per-request timeout in seconds").option("--connect-timeout <seconds>", "Connection timeout in seconds").option("--parallel", "Mark this server safe for concurrent tool calls").option("--disabled", "Save the server disabled", false).option("--ssl-verify <boolean>", "Verify HTTPS certificates: true or false").option("--client-cert <path>", "HTTP mutual TLS client certificate path").option("--client-key <path>", "HTTP mutual TLS client key path").option("--no-probe", "Save without connecting first").action(async (name, opts) => {
		const server = {};
		const command = normalizeStringifiedOptionalString(opts.command);
		const url = normalizeStringifiedOptionalString(opts.url);
		if (command && url) fail("Specify either --command for stdio or --url for HTTP, not both.");
		if (!command && !url) fail("Specify --command for stdio or --url for HTTP.");
		if (command) {
			server.command = command;
			if (opts.arg && opts.arg.length > 0) server.args = opts.arg;
			setOptionalField(server, "env", parseKeyValueEntries(opts.env, "--env"));
			setOptionalField(server, "cwd", normalizeStringifiedOptionalString(opts.cwd));
		}
		if (url) {
			server.url = url;
			setOptionalField(server, "transport", normalizeStringifiedOptionalString(opts.transport));
			setOptionalField(server, "headers", parseKeyValueEntries(opts.header, "--header"));
			const auth = normalizeLowercaseStringOrEmpty(normalizeStringifiedOptionalString(opts.auth) ?? "");
			if (auth && auth !== "oauth") fail("Invalid --auth value. Use \"oauth\".");
			if (auth) server.auth = auth;
			setOptionalField(server, "oauth", parseOAuthConfig({
				scope: opts.oauthScope,
				redirectUrl: opts.oauthRedirectUrl,
				clientMetadataUrl: opts.oauthClientMetadataUrl
			}));
			if (opts.sslVerify !== void 0) {
				const sslVerify = normalizeLowercaseStringOrEmpty(opts.sslVerify);
				if (sslVerify !== "true" && sslVerify !== "false") fail("--ssl-verify must be true or false.");
				server.sslVerify = sslVerify === "true";
			}
			setOptionalField(server, "clientCert", normalizeStringifiedOptionalString(opts.clientCert));
			setOptionalField(server, "clientKey", normalizeStringifiedOptionalString(opts.clientKey));
		}
		if (opts.disabled) server.enabled = false;
		if (opts.parallel) server.supportsParallelToolCalls = true;
		setOptionalField(server, "timeout", parsePositiveNumberOption(opts.timeout, "--timeout"));
		setOptionalField(server, "connectTimeout", parsePositiveNumberOption(opts.connectTimeout, "--connect-timeout"));
		const include = parseCsvList(opts.include);
		const exclude = parseCsvList(opts.exclude);
		if (include || exclude) server.toolFilter = {
			...include ? { include } : {},
			...exclude ? { exclude } : {}
		};
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const current = loaded.mcpServers[name];
		if (opts.probe !== false && server.enabled !== false && server.auth !== "oauth") await probeMcpServersOrFail({
			config: loaded.config,
			path: loaded.path,
			servers: { [name]: server }
		});
		const result = await setConfiguredMcpServer({
			name,
			server
		});
		if (!result.ok) fail(result.error);
		await clearStaleMcpOAuthCredentialsForReplacement({
			name,
			previous: current,
			next: server
		});
		defaultRuntime.log(`Saved MCP server "${name}" to ${result.path}.`);
		if (server.auth === "oauth") defaultRuntime.log(`Run ${formatCliCommand(`openclaw mcp login ${name}`)} to authorize this MCP server.`);
	});
	mcp.command("set").description("Set one configured MCP server from a JSON object").argument("<name>", "MCP server name").argument("<value>", "JSON object, for example {\"command\":\"uvx\",\"args\":[\"context7-mcp\"]}").action(async (name, rawValue) => {
		const parsed = parseConfigValue(rawValue);
		if (parsed.error) fail(parsed.error);
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const current = loaded.mcpServers[name];
		const result = await setConfiguredMcpServer({
			name,
			server: parsed.value
		});
		if (!result.ok) fail(result.error);
		await clearStaleMcpOAuthCredentialsForReplacement({
			name,
			previous: current,
			next: parsed.value
		});
		defaultRuntime.log(`Saved MCP server "${name}" to ${result.path}.`);
	});
	mcp.command("tools").description("Update per-server MCP tool include/exclude filters").argument("<name>", "MCP server name").option("--include <csv>", "Comma-separated MCP tool names or '*' globs to expose").option("--exclude <csv>", "Comma-separated MCP tool names or '*' globs to hide").option("--clear", "Clear this server's MCP tool filter", false).action(async (name, opts) => {
		if (!opts.clear && opts.include === void 0 && opts.exclude === void 0) fail("Specify --include, --exclude, or --clear.");
		const result = await updateConfiguredMcpServerTools({
			name,
			tools: opts.clear ? null : {
				include: parseCsvList(opts.include),
				exclude: parseCsvList(opts.exclude)
			}
		});
		if (!result.ok) fail(result.error);
		if (!result.updated) fail(`No MCP server named "${name}" in ${result.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		defaultRuntime.log(`Updated MCP tool selection for "${name}" in ${result.path}.`);
	});
	mcp.command("configure").description("Update MCP server operator controls without replacing the server").argument("<name>", "MCP server name").option("--enable", "Enable this saved server", false).option("--disable", "Disable this saved server", false).option("--include <csv>", "Comma-separated MCP tool names or '*' globs to expose").option("--exclude <csv>", "Comma-separated MCP tool names or '*' globs to hide").option("--clear-tools", "Clear this server's MCP tool filter", false).option("--timeout <seconds>", "Per-request timeout in seconds").option("--connect-timeout <seconds>", "Connection timeout in seconds").option("--clear-timeouts", "Clear request and connection timeout overrides", false).option("--parallel", "Mark this server safe for concurrent tool calls").option("--no-parallel", "Clear the concurrent tool-call marker").option("--auth <mode>", "HTTP auth mode: oauth").option("--clear-auth", "Clear auth and OAuth metadata", false).option("--oauth-scope <scope>", "OAuth scope").option("--oauth-redirect-url <url>", "OAuth redirect URL").option("--oauth-client-metadata-url <url>", "OAuth client metadata URL").option("--ssl-verify <boolean>", "Verify HTTPS certificates: true or false").option("--client-cert <path>", "HTTP mutual TLS client certificate path").option("--client-key <path>", "HTTP mutual TLS client key path").option("--clear-tls", "Clear TLS verification and mTLS overrides", false).option("--probe", "Probe the updated server before saving", false).action(async (name, opts) => {
		if (opts.enable && opts.disable) fail("Specify only one of --enable or --disable.");
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const current = loaded.mcpServers[name];
		if (!current) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		const next = { ...current };
		const clearOAuthCredentials = opts.clearAuth;
		if (opts.enable) delete next.enabled;
		if (opts.disable) next.enabled = false;
		if (opts.clearTools) delete next.toolFilter;
		else {
			const include = parseCsvList(opts.include);
			const exclude = parseCsvList(opts.exclude);
			if (include || exclude) next.toolFilter = {
				...include ? { include } : {},
				...exclude ? { exclude } : {}
			};
		}
		if (opts.clearTimeouts) {
			delete next.timeout;
			delete next.connectTimeout;
			delete next.connect_timeout;
			delete next.requestTimeoutMs;
			delete next.connectionTimeoutMs;
		}
		setOptionalField(next, "timeout", parsePositiveNumberOption(opts.timeout, "--timeout"));
		setOptionalField(next, "connectTimeout", parsePositiveNumberOption(opts.connectTimeout, "--connect-timeout"));
		if (opts.parallel === true) next.supportsParallelToolCalls = true;
		else if (opts.parallel === false) {
			delete next.supportsParallelToolCalls;
			delete next.supports_parallel_tool_calls;
		}
		if (opts.clearAuth) {
			delete next.auth;
			delete next.oauth;
		}
		const auth = normalizeLowercaseStringOrEmpty(normalizeStringifiedOptionalString(opts.auth) ?? "");
		if (auth && auth !== "oauth") fail("Invalid --auth value. Use \"oauth\".");
		if (auth) next.auth = auth;
		const oauth = parseOAuthConfig({
			scope: opts.oauthScope,
			redirectUrl: opts.oauthRedirectUrl,
			clientMetadataUrl: opts.oauthClientMetadataUrl
		});
		if (oauth) next.oauth = oauth;
		if (opts.clearTls) {
			delete next.sslVerify;
			delete next.ssl_verify;
			delete next.clientCert;
			delete next.client_cert;
			delete next.clientKey;
			delete next.client_key;
		}
		if (opts.sslVerify !== void 0) {
			const sslVerify = normalizeLowercaseStringOrEmpty(opts.sslVerify);
			if (sslVerify !== "true" && sslVerify !== "false") fail("--ssl-verify must be true or false.");
			next.sslVerify = sslVerify === "true";
		}
		setOptionalField(next, "clientCert", normalizeStringifiedOptionalString(opts.clientCert));
		setOptionalField(next, "clientKey", normalizeStringifiedOptionalString(opts.clientKey));
		if (opts.probe && next.enabled !== false && next.auth !== "oauth") await probeMcpServersOrFail({
			config: loaded.config,
			path: loaded.path,
			servers: { [name]: next }
		});
		if (opts.enable && Object.keys(next).length === 0) {
			const result = await unsetConfiguredMcpServer({ name });
			if (!result.ok) fail(result.error);
			if (clearOAuthCredentials) await clearMcpOAuthCredentialsForConfiguredServer(name, current);
			defaultRuntime.log(`Removed disabled MCP override for "${name}" in ${result.path}.`);
			return;
		}
		const result = await updateConfiguredMcpServer({
			name,
			update: () => next
		});
		if (!result.ok) fail(result.error);
		if (!result.updated) fail(`No MCP server named "${name}" in ${result.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		if (clearOAuthCredentials) await clearMcpOAuthCredentialsForConfiguredServer(name, current);
		defaultRuntime.log(`Updated MCP server "${name}" in ${result.path}.`);
	});
	mcp.command("login").description("Authorize an OAuth MCP server").argument("<name>", "MCP server name").option("--code <code>", "Authorization code from the OAuth redirect").action(async (name, opts) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const server = loaded.mcpServers[name];
		if (!server) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		if (server.auth !== "oauth") fail(`MCP server "${name}" is not configured with auth: "oauth".`);
		if (typeof server.url !== "string" || server.url.trim().length === 0) fail(`MCP server "${name}" needs a URL for OAuth login.`);
		const resolved = resolveMcpTransportConfig(name, server);
		if (!resolved || resolved.kind !== "http") fail(`MCP server "${name}" needs a valid HTTP transport for OAuth login.`);
		if (await runMcpOAuthLogin({
			serverName: name,
			serverUrl: resolved.url,
			config: server.oauth,
			authorizationCode: opts.code,
			fetchFn: withSameOriginMcpHttpHeaders({
				fetchFn: buildMcpHttpFetch({
					sslVerify: resolved.sslVerify,
					clientCert: resolved.clientCert,
					clientKey: resolved.clientKey,
					resourceUrl: resolved.url
				}),
				headers: withoutMcpAuthorizationHeader(resolved.headers),
				resourceUrl: resolved.url
			}),
			onAuthorizationUrl: (url) => {
				defaultRuntime.log(`Open this URL to authorize "${name}":`);
				defaultRuntime.log(url.toString());
				defaultRuntime.log(`After approval, run ${formatCliCommand(`openclaw mcp login ${name} --code <code>`)}.`);
			}
		}) === "authorized") defaultRuntime.log(`MCP OAuth credentials saved for "${name}".`);
	});
	mcp.command("logout").description("Clear stored OAuth credentials for an MCP server").argument("<name>", "MCP server name").action(async (name) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const server = loaded.mcpServers[name];
		if (!server) fail(`No MCP server named "${name}" in ${loaded.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		const resolved = resolveMcpTransportConfig(name, server);
		if (!resolved || resolved.kind !== "http") fail(`MCP server "${name}" needs a valid HTTP transport for OAuth logout.`);
		await clearMcpOAuthCredentials({
			serverName: name,
			serverUrl: resolved.url
		});
		defaultRuntime.log(`MCP OAuth credentials cleared for "${name}".`);
	});
	mcp.command("reload").description("Dispose cached MCP runtimes so new config is used on the next turn").action(async () => {
		const { disposeAllSessionMcpRuntimes } = await import("./agents/agent-bundle-mcp-runtime.js");
		await disposeAllSessionMcpRuntimes();
		defaultRuntime.log("Disposed cached MCP runtimes. Active agents use new MCP config on their next runtime build.");
	});
	mcp.command("unset").description("Remove one configured MCP server").argument("<name>", "MCP server name").action(async (name) => {
		const loaded = await listConfiguredMcpServers();
		if (!loaded.ok) fail(loaded.error);
		const current = loaded.mcpServers[name];
		const result = await unsetConfiguredMcpServer({ name });
		if (!result.ok) fail(result.error);
		if (!result.removed) fail(`No MCP server named "${name}" in ${result.path}. Run ${formatCliCommand("openclaw mcp list")} to see configured servers.`);
		if (current) await clearMcpOAuthCredentialsForConfiguredServer(name, current);
		defaultRuntime.log(`Removed MCP server "${name}" from ${result.path}.`);
	});
	applyParentDefaultHelpAction(mcp);
}
//#endregion
export { registerMcpCli };