Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
243d654
fix(mcp): target OpenClaw workspace config
deepujain Aug 9, 2026
4a478b8
test(mcp): verify workspace config behavior
deepujain Aug 9, 2026
e74107c
chore(mcp): format fake cli fixture
deepujain Aug 9, 2026
262468e
fix(mcp): derive workspace root from agent
deepujain Aug 9, 2026
19aacb6
refactor(mcp): resolve agent config through state
deepujain Aug 9, 2026
257c111
docs(mcp): document workspace root helpers
deepujain Aug 9, 2026
53c1804
fix(mcp): clean legacy home registrations
deepujain Aug 9, 2026
ca9273e
fix(mcp): clean default XDG registrations
deepujain Aug 9, 2026
d24f772
test(mcp): type unset XDG environment
deepujain Aug 9, 2026
67136d3
test(mcp): assert default XDG inspection status
deepujain Aug 9, 2026
0dd3d5a
test(mcp): avoid fixture existence races
deepujain Aug 10, 2026
3e450c6
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
deepujain Aug 10, 2026
ada5c2a
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
deepujain Aug 11, 2026
20d42a4
merge(main): refresh PR #8646
prekshivyas Aug 11, 2026
6a00583
test(mcp): stabilize layered config lifecycle coverage
prekshivyas Aug 11, 2026
9f368b3
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
cv Aug 11, 2026
dcd13cb
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
cv Aug 11, 2026
6104f3c
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
cv Aug 11, 2026
9304ac1
merge: reconcile concurrent PR #8646 refresh
prekshivyas Aug 11, 2026
e3abd1c
merge(main): refresh PR #8646
prekshivyas Aug 11, 2026
32a93b2
fix(mcp): tolerate absent mcporter config layers
prekshivyas Aug 11, 2026
637a6ca
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
deepujain Aug 11, 2026
5389981
test(mcp): cover layered jsonc cleanup
deepujain Aug 11, 2026
ce09bc9
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
deepujain Aug 11, 2026
7004365
fix(mcp): honor loaded OpenClaw config root
deepujain Aug 11, 2026
3959a6c
test(mcp): cover custom-root adapter lifecycle
deepujain Aug 11, 2026
eafb43c
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
deepujain Aug 11, 2026
2654d07
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
cv Aug 11, 2026
373f019
Merge branch 'main' into fix/8326-openclaw-http-mcp-config
cv Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 237 additions & 32 deletions src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import {
buildOpenClawMcporterRegisterCommand,
buildOpenClawMcporterRemoveCommand,
MCPORTER_VERSION,
OPENCLAW_MCPORTER_ROOT,
} from "./mcp-bridge-adapter-openclaw";
import {
buildOpenClawMcporterInspectCommand,
mcporterHeadersMatchExpected,
openClawMcporterRoot,
} from "./mcp-bridge-adapter-status";

const baseEntry: McpBridgeEntry = {
Expand All @@ -31,19 +33,6 @@ const baseEntry: McpBridgeEntry = {
};

describe("OpenClaw mcporter MCP adapter", () => {
it("constructs a mcporter HTTP registration with OpenShell env placeholders", () => {
const command = buildOpenClawMcporterRegisterCommand(baseEntry);

expect(command).toContain("'mcporter' 'config' 'add' 'github'");
expect(command).toContain("'--url' 'https://api.githubcopilot.com/mcp/'");
expect(command).toContain(
"'--header' 'Authorization=Bearer openshell:resolve:env:GITHUB_TOKEN'",
);
expect(command).toContain("'--scope' 'home'");
expect(command).toContain("already exists in mcporter config");
expect(command).not.toContain("fake-secret");
});

it("accepts only mcporter's synthesized HTTP Accept header in ownership checks", () => {
const expected = {
Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN",
Expand Down Expand Up @@ -89,63 +78,264 @@ describe("OpenClaw mcporter MCP adapter", () => {
).toBe(false);
});

it("uses the normalized-header ownership rule in mcporter inspect and remove commands", () => {
it("registers, inspects, and removes the OpenClaw workspace project config", () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcporter-owner-"));
try {
const fakeMcporter = path.join(temp, "mcporter");
const configState = path.join(temp, "config.json");
const homeConfigState = path.join(temp, "xdg", "mcporter", "mcporter.json");
const defaultXdgConfigState = path.join(temp, "home", ".config", "mcporter", "mcporter.json");
const legacyConfigState = path.join(temp, "legacy", "mcporter.json");
const argvLog = path.join(temp, "argv.jsonl");
const removeMarker = path.join(temp, "removed");
fs.writeFileSync(
fakeMcporter,
[
"#!/usr/bin/env node",
'const fs = require("node:fs");',
'const headers = JSON.parse(process.env.FAKE_MCPORTER_HEADERS || "{}");',
'if (process.argv[3] === "get") {',
" process.stdout.write(JSON.stringify({",
' name: "github", transport: "http",',
' baseUrl: "https://api.githubcopilot.com/mcp/", headers,',
'const path = require("node:path");',
"const args = process.argv.slice(2);",
"fs.appendFileSync(process.env.FAKE_MCPORTER_ARGV_LOG, `${JSON.stringify(args)}\\n`);",
'const configIndex = args.indexOf("config");',
"let cursor = configIndex + 1;",
"let requestedConfig;",
'if (args[cursor] === "--config") { requestedConfig = args[cursor + 1]; cursor += 2; }',
"const subcommand = configIndex >= 0 ? args[cursor] : undefined;",
"const server = args[cursor + 1];",
"const value = (flag) => args[args.indexOf(flag) + 1];",
'const root = value("--root");',
'const projectRoot = path.join(root, "config");',
"const configPath = (requested) => {",
" if (!requested) {",
" if (fs.existsSync(process.env.FAKE_MCPORTER_PROJECT_CONFIG)) return process.env.FAKE_MCPORTER_PROJECT_CONFIG;",
" if (fs.existsSync(process.env.FAKE_MCPORTER_HOME_CONFIG)) return process.env.FAKE_MCPORTER_HOME_CONFIG;",
" return process.env.FAKE_MCPORTER_LEGACY_CONFIG;",
" }",
' if (requested.startsWith(projectRoot)) return requested.endsWith(".jsonc") ? `${process.env.FAKE_MCPORTER_PROJECT_CONFIG}c` : process.env.FAKE_MCPORTER_PROJECT_CONFIG;',
" if (requested.startsWith(path.dirname(process.env.FAKE_MCPORTER_HOME_CONFIG))) return requested;",
' return requested.endsWith(".jsonc") ? `${process.env.FAKE_MCPORTER_LEGACY_CONFIG}c` : process.env.FAKE_MCPORTER_LEGACY_CONFIG;',
"};",
"const selectedConfig = configPath(requestedConfig);",
'if (subcommand === "get") {',
' if (!fs.existsSync(selectedConfig)) { console.error("not found"); process.exit(1); }',
' process.stdout.write(fs.readFileSync(selectedConfig, "utf8"));',
" process.exit(0);",
"}",
'if (subcommand === "add") {',
' const header = value("--header").split("=");',
" fs.writeFileSync(process.env.FAKE_MCPORTER_PROJECT_CONFIG, JSON.stringify({",
' name: server, transport: "http", baseUrl: value("--url"),',
' headers: { [header[0]]: header.slice(1).join("=") },',
" }));",
" process.exit(0);",
"}",
'if (process.argv[3] === "remove") {',
'if (subcommand === "remove") {',
" fs.rmSync(selectedConfig, { force: true });",
' fs.writeFileSync(process.env.FAKE_MCPORTER_REMOVE_MARKER, "removed");',
" process.exit(0);",
"}",
"process.exit(3);",
].join("\n"),
{ mode: 0o755 },
);
const run = (command: string, headers: Record<string, string>) =>
const run = (command: string) =>
spawnSync("/bin/sh", ["-c", command], {
encoding: "utf8",
env: {
...process.env,
PATH: `${temp}:${process.env.PATH ?? ""}`,
FAKE_MCPORTER_HEADERS: JSON.stringify(headers),
XDG_CONFIG_HOME: path.join(temp, "xdg"),
FAKE_MCPORTER_ARGV_LOG: argvLog,
FAKE_MCPORTER_PROJECT_CONFIG: configState,
FAKE_MCPORTER_HOME_CONFIG: homeConfigState,
FAKE_MCPORTER_LEGACY_CONFIG: legacyConfigState,
FAKE_MCPORTER_REMOVE_MARKER: removeMarker,
},
});
const runWithoutXdg = (command: string) => {
const env: NodeJS.ProcessEnv = {
...process.env,
HOME: path.join(temp, "home"),
PATH: `${temp}:${process.env.PATH ?? ""}`,
FAKE_MCPORTER_ARGV_LOG: argvLog,
FAKE_MCPORTER_PROJECT_CONFIG: configState,
FAKE_MCPORTER_HOME_CONFIG: defaultXdgConfigState,
FAKE_MCPORTER_LEGACY_CONFIG: legacyConfigState,
FAKE_MCPORTER_REMOVE_MARKER: removeMarker,
};
delete env.XDG_CONFIG_HOME;
return spawnSync("/bin/sh", ["-c", command], { encoding: "utf8", env });
};
const normalizedHeaders = {
Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN",
accept: "application/json, text/event-stream",
};
const expectFileAbsent = (filePath: string) =>
expect(fs.readdirSync(path.dirname(filePath))).not.toContain(path.basename(filePath));
const expectFilePresent = (filePath: string) =>
expect(fs.readFileSync(filePath, "utf8")).not.toBe("");

const register = run(buildOpenClawMcporterRegisterCommand(baseEntry));
expect(register.status).toBe(0);
expect(JSON.parse(fs.readFileSync(configState, "utf8"))).toEqual({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: {
Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN",
},
});

const inspect = run(buildOpenClawMcporterInspectCommand(baseEntry, true), normalizedHeaders);
const inspect = run(buildOpenClawMcporterInspectCommand(baseEntry, true));
expect(inspect.status).toBe(0);
expect(inspect.stdout.trim()).toBe("registered");

const remove = run(buildOpenClawMcporterRemoveCommand(baseEntry), normalizedHeaders);
fs.writeFileSync(
configState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: { ...normalizedHeaders, "x-unowned": "drift" },
}),
);
const drifted = run(buildOpenClawMcporterRemoveCommand(baseEntry));
expect(drifted.status).toBe(2);
expect(drifted.stderr).toContain("Refusing to remove modified mcporter MCP server");
expectFileAbsent(removeMarker);

fs.writeFileSync(
configState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: normalizedHeaders,
}),
);
const remove = run(buildOpenClawMcporterRemoveCommand(baseEntry));
expect(remove.status).toBe(0);
expect(fs.readFileSync(removeMarker, "utf8")).toBe("removed");
expectFileAbsent(configState);

fs.rmSync(removeMarker, { force: true });
const drifted = run(buildOpenClawMcporterRemoveCommand(baseEntry), {
...normalizedHeaders,
"x-unowned": "drift",
});
expect(drifted.status).toBe(2);
expect(drifted.stderr).toContain("Refusing to remove modified mcporter MCP server");
expect(fs.existsSync(removeMarker)).toBe(false);
fs.mkdirSync(path.dirname(homeConfigState), { recursive: true });
fs.writeFileSync(
homeConfigState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: normalizedHeaders,
}),
);
expect(run(buildOpenClawMcporterRegisterCommand(baseEntry, true)).status).toBe(0);
expectFilePresent(configState);
expectFilePresent(homeConfigState);

const layeredRemove = run(buildOpenClawMcporterRemoveCommand(baseEntry));
expect(layeredRemove.status).toBe(0);
expectFileAbsent(configState);
expectFileAbsent(homeConfigState);
expect(run(buildOpenClawMcporterInspectCommand(baseEntry, false)).stdout.trim()).toBe(
"absent",
);

fs.mkdirSync(path.dirname(defaultXdgConfigState), { recursive: true });
fs.writeFileSync(
defaultXdgConfigState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: normalizedHeaders,
}),
);
expect(runWithoutXdg(buildOpenClawMcporterRegisterCommand(baseEntry, true)).status).toBe(0);
const defaultXdgRemove = runWithoutXdg(buildOpenClawMcporterRemoveCommand(baseEntry));
expect(defaultXdgRemove.status).toBe(0);
expectFileAbsent(configState);
expectFileAbsent(defaultXdgConfigState);
const defaultXdgInspect = runWithoutXdg(
buildOpenClawMcporterInspectCommand(baseEntry, false),
);
expect(defaultXdgInspect.status).toBe(0);
expect(defaultXdgInspect.stdout.trim()).toBe("absent");

fs.writeFileSync(
configState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://api.githubcopilot.com/mcp/",
headers: normalizedHeaders,
}),
);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
fs.writeFileSync(
homeConfigState,
JSON.stringify({
name: "github",
transport: "http",
baseUrl: "https://user.example.test/mcp",
headers: normalizedHeaders,
}),
);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
const driftedHome = run(buildOpenClawMcporterRemoveCommand(baseEntry));
expect(driftedHome.status).toBe(2);
expectFilePresent(configState);
expectFilePresent(homeConfigState);

const observedArgs = fs
.readFileSync(argvLog, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line) as string[]);
expect(observedArgs).toContainEqual([
"--root",
OPENCLAW_MCPORTER_ROOT,
"config",
"add",
"github",
"--url",
"https://api.githubcopilot.com/mcp/",
"--header",
"Authorization=Bearer openshell:resolve:env:GITHUB_TOKEN",
"--scope",
"project",
]);
const configGets = observedArgs.filter(
(args) => args.includes("config") && args.includes("get"),
);
expect(configGets).not.toHaveLength(0);
for (const args of configGets) {
expect(args.slice(0, 3)).toEqual(["--root", OPENCLAW_MCPORTER_ROOT, "config"]);
expect(args.at(-1)).toBe("--json");
}
expect(configGets).toContainEqual([
"--root",
OPENCLAW_MCPORTER_ROOT,
"config",
"get",
"github",
"--json",
]);
expect(observedArgs).toContainEqual([
"--root",
OPENCLAW_MCPORTER_ROOT,
"config",
"--config",
`${OPENCLAW_MCPORTER_ROOT}/config/mcporter.json`,
"remove",
"github",
]);
expect(observedArgs).toContainEqual([
"--root",
OPENCLAW_MCPORTER_ROOT,
"config",
"--config",
homeConfigState,
"remove",
"github",
]);
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
Expand All @@ -161,6 +351,21 @@ describe("OpenClaw mcporter MCP adapter", () => {
expect(command).toContain("'--url' 'https://api.githubcopilot.com/mcp/'");
});

it("targets a custom OpenClaw workspace for every mcporter lifecycle command", () => {
const root = openClawMcporterRoot("/sandbox/.custom-openclaw/");
const commands = [
buildOpenClawMcporterRegisterCommand(baseEntry, false, root),
buildOpenClawMcporterInspectCommand(baseEntry, true, root),
buildOpenClawMcporterRemoveCommand(baseEntry, false, root),
];

expect(root).toBe("/sandbox/.custom-openclaw/workspace");
for (const command of commands) {
expect(command).toContain(root);
expect(command).not.toContain(OPENCLAW_MCPORTER_ROOT);
}
});

it("keeps the mcporter runtime pin visible for image tests", () => {
expect(MCPORTER_VERSION).toBe("0.7.3");
});
Expand Down
Loading