From 92d06e51bbcff265282396dbe0280f570c4e3aa0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 23:47:38 -0700 Subject: [PATCH] refactor(mcp): trim internal server exports (#108076) * refactor(mcp): remove dead internal exports * chore(deadcode): refresh export baseline --- config/knip.config.ts | 2 + scripts/deadcode-exports.baseline.mjs | 8 --- scripts/deadcode-unused-files.allowlist.mjs | 1 - src/mcp/channel-bridge.ts | 3 +- src/mcp/channel-server-runtime.ts | 68 ++++++++++++++++++ src/mcp/channel-server.test.ts | 20 +----- src/mcp/channel-server.ts | 76 +-------------------- test/scripts/check-deadcode-exports.test.ts | 4 ++ 8 files changed, 80 insertions(+), 102 deletions(-) create mode 100644 src/mcp/channel-server-runtime.ts diff --git a/config/knip.config.ts b/config/knip.config.ts index ec1468d448e4..acf8355f38bb 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -24,6 +24,8 @@ const rootEntries = [ "src/plugins/runtime/index.ts!", "src/plugins/source-display.ts!", "src/mcp/codex-supervision-tools-serve.ts!", + // Spawned by generated system-agent MCP configs; this stdio entry is not statically imported. + "src/mcp/openclaw-tools-serve.ts!", "scripts/qa/render-maturity-docs.ts!", bundledPluginFile("telegram", "src/audit.ts", "!"), bundledPluginFile("telegram", "src/token.ts", "!"), diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index fc88bcabea2c..ac845f01064b 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -235,14 +235,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/logging/diagnostic-run-activity.ts: markDiagnosticToolStartedForTest", "src/logging/redact-internal.ts: withFullContextToolPayloadRedaction", "src/logging/secret-redaction-registry.ts: resetSecretRedactionRegistryForTest", - "src/mcp/channel-bridge.ts: shouldRetryInitialMcpGatewayConnect", - "src/mcp/channel-server.ts: createOpenClawChannelMcpServer", - "src/mcp/channel-server.ts: OpenClawChannelBridge", - "src/mcp/openclaw-tools-serve-config.ts: OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_SURFACE_ENV", - "src/mcp/openclaw-tools-serve-config.ts: OpenClawToolsMcpToolId", - "src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpSystemAgentApproval", - "src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpSystemAgentSurface", - "src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpToolSelection", "src/music-generation/capabilities.ts: resolveMusicGenerationMode", "src/node-host/invoke.ts: testing", "src/plugin-state/plugin-state-store.sqlite.ts: probePluginStateStore", diff --git a/scripts/deadcode-unused-files.allowlist.mjs b/scripts/deadcode-unused-files.allowlist.mjs index 70310db884f0..15ab1df212ef 100644 --- a/scripts/deadcode-unused-files.allowlist.mjs +++ b/scripts/deadcode-unused-files.allowlist.mjs @@ -25,7 +25,6 @@ export const KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST = [ "src/gateway/gateway-cli-backend.live-helpers.ts", "src/gateway/gateway-cli-backend.live-probe-helpers.ts", "src/gateway/gateway-codex-harness.live-helpers.ts", - "src/mcp/openclaw-tools-serve.ts", "src/mcp/plugin-tools-handlers.ts", "src/mcp/plugin-tools-serve.ts", "src/mcp/tools-stdio-server.ts", diff --git a/src/mcp/channel-bridge.ts b/src/mcp/channel-bridge.ts index eb869af8af99..2195a5a48701 100644 --- a/src/mcp/channel-bridge.ts +++ b/src/mcp/channel-bridge.ts @@ -663,8 +663,7 @@ export class OpenClawChannelBridge { } } -/** Decide whether startup should wait for a retryable Gateway connect failure to recover. */ -export function shouldRetryInitialMcpGatewayConnect(error: Error): boolean { +function shouldRetryInitialMcpGatewayConnect(error: Error): boolean { if ( error.name === "GatewayClientRequestError" && "retryable" in error && diff --git a/src/mcp/channel-server-runtime.ts b/src/mcp/channel-server-runtime.ts new file mode 100644 index 000000000000..5c1c9a425bcc --- /dev/null +++ b/src/mcp/channel-server-runtime.ts @@ -0,0 +1,68 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { VERSION } from "../version.js"; +import { OpenClawChannelBridge } from "./channel-bridge.js"; +import { ClaudePermissionRequestSchema, type ClaudeChannelMode } from "./channel-shared.js"; +import { getChannelMcpCapabilities, registerChannelMcpTools } from "./channel-tools.js"; + +async function resolveMcpConfig(config: OpenClawConfig | undefined): Promise { + if (config) { + return config; + } + const { getRuntimeConfig } = await import("../config/config.js"); + return getRuntimeConfig(); +} + +export async function createChannelMcpRuntime( + opts: { + gatewayUrl?: string; + gatewayToken?: string; + gatewayPassword?: string; + config?: OpenClawConfig; + claudeChannelMode?: ClaudeChannelMode; + verbose?: boolean; + } = {}, +): Promise<{ + server: McpServer; + bridge: OpenClawChannelBridge; + start: () => Promise; + close: () => Promise; +}> { + 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 } : undefined, + ); + 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(); + }, + }; +} diff --git a/src/mcp/channel-server.test.ts b/src/mcp/channel-server.test.ts index 7f8b201f8806..71ece73134ea 100644 --- a/src/mcp/channel-server.test.ts +++ b/src/mcp/channel-server.test.ts @@ -3,9 +3,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; -import { shouldRetryInitialMcpGatewayConnect } from "./channel-bridge.js"; import { OpenClawChannelBridge } from "./channel-bridge.js"; -import { createOpenClawChannelMcpServer } from "./channel-server.js"; +import { createChannelMcpRuntime } from "./channel-server-runtime.js"; import { extractAttachmentsFromMessage } from "./channel-shared.js"; const ClaudeChannelNotificationSchema = z.object({ @@ -25,7 +24,7 @@ const ClaudePermissionNotificationSchema = z.object({ }); async function connectMcpWithoutGateway(params?: { claudeChannelMode?: "auto" | "on" | "off" }) { - const serverHarness = await createOpenClawChannelMcpServer({ + const serverHarness = await createChannelMcpRuntime({ claudeChannelMode: params?.claudeChannelMode ?? "auto", config: {} as never, verbose: false, @@ -84,22 +83,7 @@ function requireFirstMockCall(mock: { mock: { calls: unknown[][] } }, label: str return call; } -function gatewayRequestError(retryable: boolean): Error { - return Object.assign(new Error(retryable ? "gateway busy" : "auth failed"), { - name: "GatewayClientRequestError", - retryable, - }); -} - describe("openclaw channel mcp server", () => { - test("keeps initial MCP gateway connection alive through transient connect errors", () => { - expect( - shouldRetryInitialMcpGatewayConnect(new Error("gateway request timeout for connect")), - ).toBe(true); - expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(true))).toBe(true); - expect(shouldRetryInitialMcpGatewayConnect(gatewayRequestError(false))).toBe(false); - }); - describe("gateway-backed flows", () => { describe("gateway integration", () => { test("returns conversation and message payloads in primary MCP content", async () => { diff --git a/src/mcp/channel-server.ts b/src/mcp/channel-server.ts index 828b15d7db28..e90f62090267 100644 --- a/src/mcp/channel-server.ts +++ b/src/mcp/channel-server.ts @@ -1,11 +1,5 @@ -// Channel MCP server wires channel bridge tools into an MCP server instance. -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { VERSION } from "../version.js"; -import { OpenClawChannelBridge } from "./channel-bridge.js"; -import { ClaudePermissionRequestSchema, type ClaudeChannelMode } from "./channel-shared.js"; -import { getChannelMcpCapabilities, registerChannelMcpTools } from "./channel-tools.js"; +import { createChannelMcpRuntime } from "./channel-server-runtime.js"; /** * MCP stdio server assembly for OpenClaw channel conversations. @@ -13,75 +7,11 @@ import { getChannelMcpCapabilities, registerChannelMcpTools } from "./channel-to * This module wires config, the Gateway bridge, protocol notifications, and * registered tools into a lifecycle that callers can either embed or serve. */ -export { OpenClawChannelBridge } from "./channel-bridge.js"; - -/** Options accepted by the channel MCP server factory and stdio entry point. */ -type OpenClawMcpServeOptions = { - gatewayUrl?: string; - gatewayToken?: string; - gatewayPassword?: string; - config?: OpenClawConfig; - claudeChannelMode?: ClaudeChannelMode; - verbose?: boolean; -}; - -async function resolveMcpConfig(config: OpenClawConfig | undefined): Promise { - 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. */ -export async function createOpenClawChannelMcpServer(opts: OpenClawMcpServeOptions = {}): Promise<{ - server: McpServer; - bridge: OpenClawChannelBridge; - start: () => Promise; - close: () => Promise; -}> { - 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 } : undefined, - ); - 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(); - }, - }; -} +type OpenClawMcpServeOptions = NonNullable[0]>; /** Serve the channel MCP server over stdio until transport or process shutdown. */ export async function serveOpenClawChannelMcp(opts: OpenClawMcpServeOptions = {}): Promise { - const { server, start, close } = await createOpenClawChannelMcpServer(opts); + const { server, start, close } = await createChannelMcpRuntime(opts); const transport = new StdioServerTransport(); let shuttingDown = false; diff --git a/test/scripts/check-deadcode-exports.test.ts b/test/scripts/check-deadcode-exports.test.ts index ff8f9503a87a..d16ddb3e2d6a 100644 --- a/test/scripts/check-deadcode-exports.test.ts +++ b/test/scripts/check-deadcode-exports.test.ts @@ -28,6 +28,10 @@ describe("check-deadcode-exports", () => { expect(knipConfig.workspaces["."].entry).toContain("src/agents/sessions/extension-sdk.ts!"); }); + it("models the spawned system-agent MCP stdio entry", () => { + expect(knipConfig.workspaces["."].entry).toContain("src/mcp/openclaw-tools-serve.ts!"); + }); + it("parses all compact export sections and expands symbol lists", () => { expect( parseKnipCompactUnusedExports(`