Files
openclaw/src/plugins/bundle-mcp.test.ts
T
Peter Steinberger f4387b7a5e feat(plugins): support the Agent Plugins bundle format (#120115)
* feat(plugins): support the Agent Plugins bundle format

* docs(plugins): document the Agent Plugins bundle format

* test(agents): preserve agent bundle runtime discovery

* fix(plugins): isolate Agent Plugins data-dir failures and align MCP support reporting

* docs(plugins): list Agent Plugins in the canonical plugin-format guides

* fix(plugins): gate Agent Plugins detection on schema, pure inspection, root-relative cwd

* fix(plugins): record Agent Plugins data-dir ownership explicitly

* docs(plugins): cover Agent Plugins in the CLI install detection guide

* fix(plugins): carry Agent Plugins data-dir and transport contracts through external MCP projections
2026-08-07 02:55:08 -07:00

775 lines
27 KiB
TypeScript

// Verifies bundled MCP plugin metadata and package output.
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { isRecord } from "../utils.js";
import { loadEnabledBundleLspConfig } from "./bundle-lsp.js";
import { loadBundleManifest } from "./bundle-manifest.js";
import { inspectBundleMcpRuntimeSupport, loadEnabledBundleMcpConfig } from "./bundle-mcp.js";
import {
createEnabledPluginEntries,
createBundleMcpTempHarness,
createBundleProbePlugin,
withBundleHomeEnv,
writeBundleTextFiles,
writeClaudeBundleManifest,
resolveBundlePluginRoot,
} from "./bundle-mcp.test-support.js";
function getServerArgs(value: unknown): unknown[] | undefined {
return isRecord(value) && Array.isArray(value.args) ? value.args : undefined;
}
function normalizePathForAssertion(value: string | undefined): string | undefined {
if (!value) {
return value;
}
return path.normalize(value).replace(/\\/g, "/");
}
async function expectResolvedPathEqual(actual: unknown, expected: string): Promise<void> {
expect(typeof actual).toBe("string");
if (typeof actual !== "string") {
return;
}
expect(normalizePathForAssertion(await fs.realpath(actual))).toBe(
normalizePathForAssertion(await fs.realpath(expected)),
);
}
async function expectPathMissing(targetPath: string): Promise<void> {
await expect(fs.stat(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
}
function expectNoDiagnostics(diagnostics: unknown[]) {
expect(diagnostics).toStrictEqual([]);
}
const tempHarness = createBundleMcpTempHarness();
afterEach(async () => {
await tempHarness.cleanup();
});
function createEnabledBundleConfig(pluginIds: string[]): OpenClawConfig {
return {
plugins: {
entries: createEnabledPluginEntries(pluginIds),
},
};
}
const AGENT_PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
const AGENT_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
async function writeAgentBundle(params: {
homeDir: string;
pluginId: string;
manifest?: Record<string, unknown>;
mcp?: unknown;
textFiles?: Record<string, string>;
}) {
const pluginRoot = resolveBundlePluginRoot(params.homeDir, params.pluginId);
await writeBundleTextFiles(pluginRoot, {
"plugin.json": `${JSON.stringify({ $schema: AGENT_PLUGIN_SCHEMA, name: params.pluginId, ...params.manifest }, null, 2)}\n`,
...(params.mcp === undefined ? {} : { "mcp.json": `${JSON.stringify(params.mcp, null, 2)}\n` }),
...params.textFiles,
});
return pluginRoot;
}
async function expectInlineBundleMcpServer(params: {
loadedServer: unknown;
pluginRoot: string;
commandRelativePath: string;
argRelativePaths: readonly string[];
}) {
const loadedArgs = getServerArgs(params.loadedServer);
const loadedCommand = isRecord(params.loadedServer) ? params.loadedServer.command : undefined;
const loadedCwd = isRecord(params.loadedServer) ? params.loadedServer.cwd : undefined;
const loadedEnv =
isRecord(params.loadedServer) && isRecord(params.loadedServer.env)
? params.loadedServer.env
: {};
await expectResolvedPathEqual(loadedCwd, params.pluginRoot);
expect(typeof loadedCommand).toBe("string");
expect(loadedArgs).toHaveLength(params.argRelativePaths.length);
expect(typeof loadedEnv.PLUGIN_ROOT).toBe("string");
if (typeof loadedCommand !== "string" || typeof loadedCwd !== "string") {
throw new Error("expected inline bundled MCP server to expose command and cwd");
}
expect(normalizePathForAssertion(path.relative(loadedCwd, loadedCommand))).toBe(
normalizePathForAssertion(params.commandRelativePath),
);
expect(
loadedArgs?.map((entry) =>
typeof entry === "string"
? normalizePathForAssertion(path.relative(loadedCwd, entry))
: entry,
),
).toEqual([...params.argRelativePaths]);
await expectResolvedPathEqual(loadedEnv.PLUGIN_ROOT, params.pluginRoot);
}
describe("loadEnabledBundleMcpConfig", () => {
it("loads enabled Claude bundle MCP config and absolutizes relative args", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-mcp",
async ({ homeDir, workspaceDir }) => {
const { pluginRoot, serverPath } = await createBundleProbePlugin(homeDir);
const config: OpenClawConfig = {
plugins: {
entries: {
"bundle-probe": { enabled: true },
},
},
};
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: config,
});
const resolvedServerPath = await fs.realpath(serverPath);
const loadedServer = expectDefined(
loaded.config.mcpServers.bundleProbe,
"loaded.config.mcpServers.bundleProbe test invariant",
);
const loadedArgs = getServerArgs(loadedServer);
const loadedServerPath = typeof loadedArgs?.[0] === "string" ? loadedArgs[0] : undefined;
const resolvedPluginRoot = await fs.realpath(pluginRoot);
expectNoDiagnostics(loaded.diagnostics);
expect(isRecord(loadedServer) ? loadedServer.command : undefined).toBe("node");
expect(loadedArgs).toHaveLength(1);
if (!loadedServerPath) {
throw new Error("expected bundled MCP args to include the server path");
}
expect(normalizePathForAssertion(await fs.realpath(loadedServerPath))).toBe(
normalizePathForAssertion(resolvedServerPath),
);
await expectResolvedPathEqual(loadedServer.cwd, resolvedPluginRoot);
},
);
});
it("uses a provided manifest registry instead of rediscovering bundle plugins", async () => {
const homeDir = await tempHarness.createTempDir("openclaw-bundle-mcp-home-");
const workspaceDir = await tempHarness.createTempDir("openclaw-bundle-mcp-workspace-");
const { pluginRoot } = await createBundleProbePlugin(homeDir);
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["bundle-probe"]),
manifestRegistry: {
plugins: [
{
id: "bundle-probe",
origin: "global",
format: "bundle",
bundleFormat: "claude",
channels: [],
providers: [],
cliBackends: [],
skills: [],
hooks: [],
rootDir: await fs.realpath(pluginRoot),
source: "test",
manifestPath: path.join(pluginRoot, ".claude-plugin", "plugin.json"),
},
],
},
});
expectNoDiagnostics(loaded.diagnostics);
expect(loaded.config.mcpServers.bundleProbe).toMatchObject({
command: "node",
});
});
it("loads MCP servers declared by an enabled native plugin", async () => {
const workspaceDir = await tempHarness.createTempDir("openclaw-native-mcp-workspace-");
const pluginRoot = await tempHarness.createTempDir("openclaw-native-mcp-plugin-");
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["native-mcp"]),
manifestRegistry: {
plugins: [
{
id: "native-mcp",
origin: "global",
format: "openclaw",
channels: [],
providers: [],
cliBackends: [],
skills: [],
hooks: [],
rootDir: pluginRoot,
source: path.join(pluginRoot, "index.js"),
manifestPath: path.join(pluginRoot, "openclaw.plugin.json"),
mcpServers: {
app: {
transport: "stdio",
command: "node",
args: ["./mcp-server.js"],
},
},
},
],
},
});
expectNoDiagnostics(loaded.diagnostics);
expect(loaded.config.mcpServers.app).toEqual({
transport: "stdio",
command: "node",
args: [path.join(pluginRoot, "mcp-server.js")],
cwd: pluginRoot,
});
});
it("skips MCP servers declared by a disabled native plugin", async () => {
const workspaceDir = await tempHarness.createTempDir("openclaw-native-mcp-workspace-");
const pluginRoot = await tempHarness.createTempDir("openclaw-native-mcp-plugin-");
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: { plugins: { entries: { "native-mcp": { enabled: false } } } },
manifestRegistry: {
plugins: [
{
id: "native-mcp",
origin: "global",
format: "openclaw",
channels: [],
providers: [],
cliBackends: [],
skills: [],
hooks: [],
rootDir: pluginRoot,
source: path.join(pluginRoot, "index.js"),
manifestPath: path.join(pluginRoot, "openclaw.plugin.json"),
mcpServers: { app: { command: "node", args: ["./mcp-server.js"] } },
},
],
},
});
expectNoDiagnostics(loaded.diagnostics);
expect(loaded.config.mcpServers).toStrictEqual({});
});
it("merges inline bundle MCP servers and skips disabled bundles", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-inline",
async ({ homeDir, workspaceDir }) => {
await writeClaudeBundleManifest({
homeDir,
pluginId: "inline-enabled",
manifest: {
name: "inline-enabled",
mcpServers: {
enabledProbe: {
command: "node",
args: ["./enabled.mjs"],
},
},
},
});
await writeClaudeBundleManifest({
homeDir,
pluginId: "inline-disabled",
manifest: {
name: "inline-disabled",
mcpServers: {
disabledProbe: {
command: "node",
args: ["./disabled.mjs"],
},
},
},
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: {
plugins: {
entries: {
...createEnabledPluginEntries(["inline-enabled"]),
"inline-disabled": { enabled: false },
},
},
},
});
const enabledProbe = loaded.config.mcpServers.enabledProbe;
const enabledArgs = getServerArgs(enabledProbe);
expect(isRecord(enabledProbe) ? enabledProbe.command : undefined).toBe("node");
expect(enabledArgs).toHaveLength(1);
expect(typeof enabledArgs?.[0]).toBe("string");
if (typeof enabledArgs?.[0] !== "string") {
throw new Error("expected inline MCP enabledProbe args to include enabled.mjs");
}
expect(enabledArgs[0]).toContain("enabled.mjs");
expect(loaded.config.mcpServers.disabledProbe).toBeUndefined();
},
);
});
it("resolves inline Claude MCP paths from the plugin root and expands CLAUDE_PLUGIN_ROOT", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-inline-placeholder",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeClaudeBundleManifest({
homeDir,
pluginId: "inline-claude",
manifest: {
name: "inline-claude",
mcpServers: {
inlineProbe: {
command: "${CLAUDE_PLUGIN_ROOT}/bin/server.sh",
args: ["${CLAUDE_PLUGIN_ROOT}/servers/probe.mjs", "./local-probe.mjs"],
cwd: "${CLAUDE_PLUGIN_ROOT}",
env: {
PLUGIN_ROOT: "${CLAUDE_PLUGIN_ROOT}",
},
},
},
},
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["inline-claude"]),
});
const loadedServer = loaded.config.mcpServers.inlineProbe;
expectNoDiagnostics(loaded.diagnostics);
await expectInlineBundleMcpServer({
loadedServer,
pluginRoot,
commandRelativePath: path.join("bin", "server.sh"),
argRelativePaths: [
normalizePathForAssertion(path.join("servers", "probe.mjs"))!,
normalizePathForAssertion("local-probe.mjs")!,
],
});
},
);
});
it("loads Link-style Codex bundle MCP config", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-link",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = resolveBundlePluginRoot(homeDir, "link");
await writeBundleTextFiles(pluginRoot, {
".codex-plugin/plugin.json": `${JSON.stringify(
{
name: "link",
skills: "./skills/",
mcpServers: "./.mcp.json",
},
null,
2,
)}\n`,
".mcp.json": `${JSON.stringify(
{
mcpServers: {
link: {
command: "pnpx",
args: ["@stripe/link-cli", "--mcp"],
},
},
},
null,
2,
)}\n`,
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["link"]),
});
const loadedServer = loaded.config.mcpServers.link;
expectNoDiagnostics(loaded.diagnostics);
expect(isRecord(loadedServer) ? loadedServer.command : undefined).toBe("pnpx");
expect(getServerArgs(loadedServer)).toEqual(["@stripe/link-cli", "--mcp"]);
await expectResolvedPathEqual(
isRecord(loadedServer) ? loadedServer.cwd : undefined,
pluginRoot,
);
},
);
});
it("reports malformed file-backed MCP configs instead of silently dropping servers", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-malformed-mcp",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeClaudeBundleManifest({
homeDir,
pluginId: "malformed-mcp",
manifest: {
name: "malformed-mcp",
mcpServers: ".mcp.json",
},
});
await fs.writeFile(path.join(pluginRoot, ".mcp.json"), "{", "utf-8");
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["malformed-mcp"]),
});
expect(loaded.config.mcpServers).toStrictEqual({});
expect(loaded.diagnostics).toHaveLength(1);
expect(loaded.diagnostics[0]?.pluginId).toBe("malformed-mcp");
expect(loaded.diagnostics[0]?.message).toContain("unable to read .mcp.json");
},
);
});
it("reports malformed file-backed LSP configs instead of silently dropping servers", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-bundle-malformed-lsp",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeClaudeBundleManifest({
homeDir,
pluginId: "malformed-lsp",
manifest: {
name: "malformed-lsp",
lspServers: ".lsp.json",
},
});
await fs.writeFile(path.join(pluginRoot, ".lsp.json"), "{", "utf-8");
const loaded = loadEnabledBundleLspConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["malformed-lsp"]),
});
expect(loaded.config.lspServers).toStrictEqual({});
expect(loaded.diagnostics).toHaveLength(1);
expect(loaded.diagnostics[0]?.pluginId).toBe("malformed-lsp");
expect(loaded.diagnostics[0]?.message).toContain("unable to read .lsp.json");
},
);
});
it("loads Agent Plugins MCP config with placeholders, injected env, and canonical transports", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-mcp",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeAgentBundle({
homeDir,
pluginId: "portable-mcp",
mcp: {
$schema: AGENT_MCP_SCHEMA,
mcpServers: {
local: {
type: "stdio",
command: "./bin/server",
args: ["${PLUGIN_ROOT}/config.json", "${PLUGIN_DATA}/cache"],
env: {
ROOT_COPY: "${PLUGIN_ROOT}",
DATA_COPY: "${PLUGIN_DATA}",
},
cwd: "${PLUGIN_DATA}",
},
remote: {
type: "streamable-http",
url: "https://example.test/mcp",
headers: { Authorization: "Bearer test" },
},
legacy: {
type: "sse",
url: "https://example.test/sse",
},
},
},
textFiles: {
"bin/server": "#!/bin/sh\n",
"config.json": "{}\n",
},
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["portable-mcp"]),
});
const local = expectDefined(loaded.config.mcpServers.local, "local agent MCP server");
const remote = expectDefined(loaded.config.mcpServers.remote, "remote agent MCP server");
const legacy = expectDefined(loaded.config.mcpServers.legacy, "legacy agent MCP server");
const localEnv = isRecord(local.env) ? local.env : {};
const localArgs = getServerArgs(local);
expectNoDiagnostics(loaded.diagnostics);
expect(local).toMatchObject({ transport: "stdio" });
expect(local.type).toBeUndefined();
await expectResolvedPathEqual(local.command, path.join(pluginRoot, "bin", "server"));
await expectResolvedPathEqual(localArgs?.[0], path.join(pluginRoot, "config.json"));
expect(localArgs?.[1]).toBe(path.join(String(localEnv.PLUGIN_DATA), "cache"));
await expectResolvedPathEqual(localEnv.PLUGIN_ROOT, pluginRoot);
const pluginDataPath = path.join(homeDir, ".openclaw", "plugin-data", "portable-mcp");
expect(localEnv.PLUGIN_DATA).toBe(pluginDataPath);
expect(local.cwd).toBe(pluginDataPath);
expect(loaded.prepareDataDirsByServer).toEqual({
local: { pluginId: "portable-mcp", dataDir: pluginDataPath },
});
await expectPathMissing(pluginDataPath);
expect(localEnv.ROOT_COPY).toBe(localEnv.PLUGIN_ROOT);
expect(localEnv.DATA_COPY).toBe(localEnv.PLUGIN_DATA);
expect(remote).toEqual({
transport: "streamable-http",
url: "https://example.test/mcp",
headers: { Authorization: "Bearer test" },
});
expect(legacy).toEqual({
transport: "sse",
url: "https://example.test/sse",
});
expect(
inspectBundleMcpRuntimeSupport({
pluginId: "portable-mcp",
rootDir: pluginRoot,
bundleFormat: "agent",
}),
).toMatchObject({
hasSupportedStdioServer: true,
supportedServerNames: ["local", "remote", "legacy"],
stdioServerNames: ["local"],
unsupportedServerNames: [],
});
},
);
});
it("resolves Agent Plugins relative cwd from the plugin root and rejects traversal", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-cwd",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeAgentBundle({
homeDir,
pluginId: "portable-cwd",
mcp: {
$schema: AGENT_MCP_SCHEMA,
mcpServers: {
valid: { type: "stdio", command: "node", cwd: "./child" },
relativeEscape: { type: "stdio", command: "node", cwd: "./../escape" },
placeholderEscape: {
type: "stdio",
command: "node",
cwd: "${PLUGIN_ROOT}/../escape",
},
},
},
textFiles: { "child/.keep": "" },
});
const rootRealPath = await fs.realpath(pluginRoot);
const relativeProcessCwd = path.relative(rootRealPath, await fs.realpath(process.cwd()));
expect(relativeProcessCwd === ".." || relativeProcessCwd.startsWith(`..${path.sep}`)).toBe(
true,
);
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["portable-cwd"]),
});
expect(Object.keys(loaded.config.mcpServers)).toEqual(["valid"]);
await expectResolvedPathEqual(
loaded.config.mcpServers.valid?.cwd,
path.join(pluginRoot, "child"),
);
expect(loaded.diagnostics).toHaveLength(2);
expect(loaded.diagnostics.map((entry) => entry.message)).toEqual([
expect.stringContaining('invalid MCP server "relativeEscape"'),
expect.stringContaining('invalid MCP server "placeholderEscape"'),
]);
expect(loaded.diagnostics.every((entry) => entry.message.includes("cwd must remain"))).toBe(
true,
);
},
);
});
it("ignores dot MCP config and inline MCP fields for Agent Plugins", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-closed",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeAgentBundle({
homeDir,
pluginId: "closed-agent",
manifest: {
mcpServers: {
inline: { type: "stdio", command: "node" },
},
},
textFiles: {
".mcp.json": JSON.stringify({
mcpServers: { dotted: { type: "stdio", command: "node" } },
}),
},
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["closed-agent"]),
});
expectNoDiagnostics(loaded.diagnostics);
expect(loaded.config.mcpServers).toStrictEqual({});
await expectPathMissing(path.join(homeDir, ".openclaw", "plugin-data", "closed-agent"));
expect(await fs.realpath(pluginRoot)).toBeTruthy();
},
);
});
it("keeps Agent Plugins inspection pure when PLUGIN_DATA collides", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-data-collision",
async ({ homeDir, workspaceDir }) => {
const pluginId = "data-dir-collision";
const pluginRoot = await writeAgentBundle({
homeDir,
pluginId,
mcp: {
$schema: AGENT_MCP_SCHEMA,
mcpServers: {
local: { type: "stdio", command: "node" },
remote: { type: "streamable-http", url: "https://example.test/mcp" },
},
},
textFiles: {
"skills/weather/SKILL.md": "---\nname: weather\ndescription: Weather skill\n---\n",
},
});
const pluginDataPath = path.join(homeDir, ".openclaw", "plugin-data", pluginId);
await fs.mkdir(path.dirname(pluginDataPath), { recursive: true });
await fs.writeFile(pluginDataPath, "directory collision", "utf8");
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig([pluginId]),
});
expect(loaded.config.mcpServers).toEqual({
local: {
transport: "stdio",
command: "node",
cwd: await fs.realpath(pluginRoot),
env: {
PLUGIN_ROOT: await fs.realpath(pluginRoot),
PLUGIN_DATA: pluginDataPath,
},
},
remote: { transport: "streamable-http", url: "https://example.test/mcp" },
});
expect(loaded.diagnostics).toStrictEqual([]);
expect(loaded.prepareDataDirsByServer).toEqual({
local: { pluginId, dataDir: pluginDataPath },
});
expect(
inspectBundleMcpRuntimeSupport({
pluginId,
rootDir: pluginRoot,
bundleFormat: "agent",
}),
).toMatchObject({
supportedServerNames: ["local", "remote"],
unsupportedServerNames: [],
});
expect((await fs.stat(pluginDataPath)).isFile()).toBe(true);
const manifest = loadBundleManifest({ rootDir: pluginRoot, bundleFormat: "agent" });
expect(manifest.ok).toBe(true);
if (manifest.ok) {
expect(manifest.manifest.skills).toEqual(["skills"]);
expect(manifest.manifest.capabilities).toEqual(
expect.arrayContaining(["skills", "mcpServers"]),
);
}
},
);
});
it.each([
{ name: "malformed JSON", content: "{" },
{ name: "missing mcpServers", content: JSON.stringify({ $schema: AGENT_MCP_SCHEMA }) },
])("isolates Agent Plugins MCP failure for $name", async ({ content }) => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-invalid",
async ({ homeDir, workspaceDir }) => {
const pluginRoot = await writeAgentBundle({
homeDir,
pluginId: "invalid-agent-mcp",
});
await fs.writeFile(path.join(pluginRoot, "mcp.json"), content, "utf-8");
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["invalid-agent-mcp"]),
});
expect(loaded.config.mcpServers).toStrictEqual({});
expect(loaded.diagnostics).toHaveLength(1);
expect(loaded.diagnostics[0]?.pluginId).toBe("invalid-agent-mcp");
expect(loaded.diagnostics[0]?.message).toContain("mcp.json");
},
);
});
it("skips invalid Agent Plugins MCP entries while retaining valid siblings", async () => {
await withBundleHomeEnv(
tempHarness,
"openclaw-agent-bundle-entry-isolation",
async ({ homeDir, workspaceDir }) => {
await writeAgentBundle({
homeDir,
pluginId: "isolated-agent-mcp",
mcp: {
$schema: AGENT_MCP_SCHEMA,
mcpServers: {
valid: { type: "streamable-http", url: "https://example.test/mcp" },
invalid: {
type: "stdio",
command: "node --inspect",
env: { PLUGIN_ROOT: "override" },
},
},
},
});
const loaded = loadEnabledBundleMcpConfig({
workspaceDir,
cfg: createEnabledBundleConfig(["isolated-agent-mcp"]),
});
expect(loaded.config.mcpServers).toEqual({
valid: {
transport: "streamable-http",
url: "https://example.test/mcp",
},
});
expect(loaded.diagnostics).toHaveLength(1);
expect(loaded.diagnostics[0]?.message).toContain('invalid MCP server "invalid"');
},
);
});
});