Plugins: surface MCP servers and bundle capabilities in inspect reports

This commit is contained in:
Vincent Koc 2026-03-17 23:12:48 -07:00
parent b9b891b614
commit b48413e252
4 changed files with 156 additions and 2 deletions

View File

@ -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)]));
}

View File

@ -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,
};

View File

@ -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",

View File

@ -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,