diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 8342b6c58b3..8e02bff7a47 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -660,6 +660,8 @@ export function registerPluginsCli(program: Command) { .map((entry) => (entry.severity === "warn" ? `warn:${entry.code}` : entry.code)) .join(", ") : "none", + Bundle: + inspect.bundleCapabilities.length > 0 ? inspect.bundleCapabilities.join(", ") : "-", Hooks: formatHookSummary({ usesLegacyBeforeAgentStart: inspect.usesLegacyBeforeAgentStart, typedHookCount: inspect.typedHooks.length, @@ -676,6 +678,7 @@ export function registerPluginsCli(program: Command) { { key: "Shape", header: "Shape", minWidth: 18 }, { key: "Capabilities", header: "Capabilities", minWidth: 28, flex: true }, { key: "Compatibility", header: "Compatibility", minWidth: 24, flex: true }, + { key: "Bundle", header: "Bundle", minWidth: 14, flex: true }, { key: "Hooks", header: "Hooks", minWidth: 20, flex: true }, ], rows, @@ -738,9 +741,9 @@ export function registerPluginsCli(program: Command) { lines.push( `${theme.muted("Legacy before_agent_start:")} ${inspect.usesLegacyBeforeAgentStart ? "yes" : "no"}`, ); - if ((inspect.plugin.bundleCapabilities?.length ?? 0) > 0) { + if (inspect.bundleCapabilities.length > 0) { lines.push( - `${theme.muted("Bundle capabilities:")} ${inspect.plugin.bundleCapabilities?.join(", ")}`, + `${theme.muted("Bundle capabilities:")} ${inspect.bundleCapabilities.join(", ")}`, ); } lines.push( @@ -785,6 +788,14 @@ export function registerPluginsCli(program: Command) { lines.push(...formatInspectSection("CLI commands", inspect.cliCommands)); lines.push(...formatInspectSection("Services", inspect.services)); lines.push(...formatInspectSection("Gateway methods", inspect.gatewayMethods)); + lines.push( + ...formatInspectSection( + "MCP servers", + inspect.mcpServers.map((entry) => + entry.hasStdioTransport ? entry.name : `${entry.name} (unsupported transport)`, + ), + ), + ); if (inspect.httpRouteCount > 0) { lines.push(...formatInspectSection("HTTP routes", [String(inspect.httpRouteCount)])); } diff --git a/src/plugins/bundle-mcp.ts b/src/plugins/bundle-mcp.ts index fbd733d9695..b0960c17a93 100644 --- a/src/plugins/bundle-mcp.ts +++ b/src/plugins/bundle-mcp.ts @@ -32,6 +32,7 @@ export type EnabledBundleMcpConfigResult = { }; export type BundleMcpRuntimeSupport = { hasSupportedStdioServer: boolean; + supportedServerNames: string[]; unsupportedServerNames: string[]; diagnostics: string[]; }; @@ -279,17 +280,20 @@ export function inspectBundleMcpRuntimeSupport(params: { bundleFormat: PluginBundleFormat; }): BundleMcpRuntimeSupport { const loaded = loadBundleMcpConfig(params); + const supportedServerNames: string[] = []; 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; + supportedServerNames.push(serverName); continue; } unsupportedServerNames.push(serverName); } return { hasSupportedStdioServer, + supportedServerNames, unsupportedServerNames, diagnostics: loaded.diagnostics, }; diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 7d93c52bc21..ad895899dc5 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -493,6 +493,117 @@ describe("buildPluginStatusReport", () => { expect(buildPluginCompatibilityWarnings()).toEqual([]); }); + it("populates bundleCapabilities from plugin record", () => { + loadOpenClawPluginsMock.mockReturnValue({ + plugins: [ + { + id: "claude-bundle", + name: "Claude Bundle", + description: "A bundle plugin with skills and commands", + source: "/tmp/claude-bundle/.claude-plugin/plugin.json", + origin: "workspace", + enabled: true, + status: "loaded", + format: "bundle", + bundleFormat: "claude", + bundleCapabilities: ["skills", "commands", "agents", "settings"], + rootDir: "/tmp/claude-bundle", + toolNames: [], + hookNames: [], + channelIds: [], + providerIds: [], + speechProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + webSearchProviderIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + }, + ], + diagnostics: [], + channels: [], + channelSetups: [], + providers: [], + speechProviders: [], + mediaUnderstandingProviders: [], + imageGenerationProviders: [], + webSearchProviders: [], + tools: [], + hooks: [], + typedHooks: [], + httpRoutes: [], + gatewayHandlers: {}, + cliRegistrars: [], + services: [], + commands: [], + }); + + const inspect = buildPluginInspectReport({ id: "claude-bundle" }); + + expect(inspect).not.toBeNull(); + expect(inspect?.bundleCapabilities).toEqual(["skills", "commands", "agents", "settings"]); + expect(inspect?.mcpServers).toEqual([]); + expect(inspect?.shape).toBe("non-capability"); + }); + + it("returns empty bundleCapabilities and mcpServers for non-bundle plugins", () => { + loadOpenClawPluginsMock.mockReturnValue({ + plugins: [ + { + id: "plain-plugin", + name: "Plain Plugin", + description: "A regular plugin", + source: "/tmp/plain-plugin/index.ts", + origin: "workspace", + enabled: true, + status: "loaded", + toolNames: [], + hookNames: [], + channelIds: [], + providerIds: ["plain"], + speechProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + webSearchProviderIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + }, + ], + diagnostics: [], + channels: [], + channelSetups: [], + providers: [], + speechProviders: [], + mediaUnderstandingProviders: [], + imageGenerationProviders: [], + webSearchProviders: [], + tools: [], + hooks: [], + typedHooks: [], + httpRoutes: [], + gatewayHandlers: {}, + cliRegistrars: [], + services: [], + commands: [], + }); + + const inspect = buildPluginInspectReport({ id: "plain-plugin" }); + + expect(inspect).not.toBeNull(); + expect(inspect?.bundleCapabilities).toEqual([]); + expect(inspect?.mcpServers).toEqual([]); + }); + it("formats and summarizes compatibility notices", () => { const notice = { pluginId: "legacy-plugin", diff --git a/src/plugins/status.ts b/src/plugins/status.ts index ad747d375bd..51284e43d42 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -2,6 +2,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; import { normalizePluginsConfig } from "./config-state.js"; import { loadOpenClawPlugins } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; @@ -64,7 +65,12 @@ export type PluginInspectReport = { cliCommands: string[]; services: string[]; gatewayMethods: string[]; + mcpServers: Array<{ + name: string; + hasStdioTransport: boolean; + }>; httpRouteCount: number; + bundleCapabilities: string[]; diagnostics: PluginDiagnostic[]; policy: { allowPromptInjection?: boolean; @@ -226,6 +232,26 @@ export function buildPluginInspectReport(params: { httpRouteCount: plugin.httpRoutes, }); + // Populate MCP server info for bundle-format plugins with a known rootDir. + let mcpServers: PluginInspectReport["mcpServers"] = []; + if (plugin.format === "bundle" && plugin.bundleFormat && plugin.rootDir) { + const mcpSupport = inspectBundleMcpRuntimeSupport({ + pluginId: plugin.id, + rootDir: plugin.rootDir, + bundleFormat: plugin.bundleFormat, + }); + mcpServers = [ + ...mcpSupport.supportedServerNames.map((name) => ({ + name, + hasStdioTransport: true, + })), + ...mcpSupport.unsupportedServerNames.map((name) => ({ + name, + hasStdioTransport: false, + })), + ]; + } + const usesLegacyBeforeAgentStart = typedHooks.some( (entry) => entry.name === "before_agent_start", ); @@ -248,7 +274,9 @@ export function buildPluginInspectReport(params: { cliCommands: [...plugin.cliCommands], services: [...plugin.services], gatewayMethods: [...plugin.gatewayMethods], + mcpServers, httpRouteCount: plugin.httpRoutes, + bundleCapabilities: plugin.bundleCapabilities ?? [], diagnostics, policy: { allowPromptInjection: policyEntry?.hooks?.allowPromptInjection,