diff --git a/src/plugins/bundle-mcp.ts b/src/plugins/bundle-mcp.ts index 179254f4dbc..29bd2b3a6c9 100644 --- a/src/plugins/bundle-mcp.ts +++ b/src/plugins/bundle-mcp.ts @@ -28,6 +28,11 @@ export type EnabledBundleMcpConfigResult = { config: BundleMcpConfig; diagnostics: BundleMcpDiagnostic[]; }; +export type BundleMcpRuntimeSupport = { + hasSupportedStdioServer: boolean; + unsupportedServerNames: string[]; + diagnostics: string[]; +}; const MANIFEST_PATH_BY_FORMAT: Record = { claude: CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, @@ -292,6 +297,28 @@ function loadBundleMcpConfig(params: { return { config: merged, diagnostics: [] }; } +export function inspectBundleMcpRuntimeSupport(params: { + pluginId: string; + rootDir: string; + bundleFormat: PluginBundleFormat; +}): BundleMcpRuntimeSupport { + const loaded = loadBundleMcpConfig(params); + const unsupportedServerNames: string[] = []; + let hasSupportedStdioServer = false; + for (const [serverName, server] of Object.entries(loaded.config.mcpServers)) { + if (typeof server.command === "string" && server.command.trim().length > 0) { + hasSupportedStdioServer = true; + continue; + } + unsupportedServerNames.push(serverName); + } + return { + hasSupportedStdioServer, + unsupportedServerNames, + diagnostics: loaded.diagnostics, + }; +} + export function loadEnabledBundleMcpConfig(params: { workspaceDir: string; cfg?: OpenClawConfig; diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index da070a207d5..a1e25c0ea3e 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -471,6 +471,65 @@ describe("bundle plugins", () => { ).toBe(false); }); + it("warns when bundle MCP only declares unsupported non-stdio transports", () => { + useNoBundledPlugins(); + const workspaceDir = makeTempDir(); + const stateDir = makeTempDir(); + const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", "claude-mcp-url"); + fs.mkdirSync(path.join(bundleRoot, ".claude-plugin"), { recursive: true }); + fs.writeFileSync( + path.join(bundleRoot, ".claude-plugin", "plugin.json"), + JSON.stringify({ + name: "Claude MCP URL", + }), + "utf-8", + ); + fs.writeFileSync( + path.join(bundleRoot, ".mcp.json"), + JSON.stringify({ + mcpServers: { + remoteProbe: { + url: "http://127.0.0.1:8787/mcp", + }, + }, + }), + "utf-8", + ); + + const registry = withEnv( + { + OPENCLAW_HOME: stateDir, + OPENCLAW_STATE_DIR: stateDir, + }, + () => + loadOpenClawPlugins({ + workspaceDir, + config: { + plugins: { + entries: { + "claude-mcp-url": { + enabled: true, + }, + }, + }, + }, + cache: false, + }), + ); + + const plugin = registry.plugins.find((entry) => entry.id === "claude-mcp-url"); + expect(plugin?.status).toBe("loaded"); + expect(plugin?.bundleCapabilities).toEqual(expect.arrayContaining(["mcpServers"])); + expect( + registry.diagnostics.some( + (diag) => + diag.pluginId === "claude-mcp-url" && + diag.message.includes("stdio only today") && + diag.message.includes("remoteProbe"), + ), + ).toBe(true); + }); + it("treats Cursor command roots as supported bundle skill surfaces", () => { useNoBundledPlugins(); const workspaceDir = makeTempDir(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 5f1b651b72d..86273793006 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -11,6 +11,7 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveUserPath } from "../utils.js"; +import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; import { clearPluginCommands } from "./commands.js"; import { applyTestPluginDefaults, @@ -1115,6 +1116,36 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi message: `bundle capability detected but not wired into OpenClaw yet: ${capability}`, }); } + if ( + enableState.enabled && + record.rootDir && + record.bundleFormat && + (record.bundleCapabilities ?? []).includes("mcpServers") + ) { + const runtimeSupport = inspectBundleMcpRuntimeSupport({ + pluginId: record.id, + rootDir: record.rootDir, + bundleFormat: record.bundleFormat, + }); + for (const message of runtimeSupport.diagnostics) { + registry.diagnostics.push({ + level: "warn", + pluginId: record.id, + source: record.source, + message, + }); + } + if (runtimeSupport.unsupportedServerNames.length > 0) { + registry.diagnostics.push({ + level: "warn", + pluginId: record.id, + source: record.source, + message: + "bundle MCP servers use unsupported transports or incomplete configs " + + `(stdio only today): ${runtimeSupport.unsupportedServerNames.join(", ")}`, + }); + } + } registry.plugins.push(record); seenIds.set(pluginId, candidate.origin); continue;