Files
can1357 b94bfba025 feat(mcp): enforced header precedence and origin policy on remote transports
- Client-generated HTTP/MCP/authorization headers win over configured
  headers case-insensitively (Agent Plugins §7.2.1) via the new
  header-policy fetch wrapper used by the HTTP and legacy SSE transports.
- headerPolicy: "origin-locked" pins configured headers to the configured
  URL's origin: never forwarded across cross-origin redirects, and
  method-changing redirects of JSON-RPC POSTs are refused.
- envPolicy: "literal" exempts stdio env values (and origin-locked
  headers) from config-value resolution: no ambient env-name lookup, no
  __omp_shell("command execution, empty values preserved.")
2026-08-07 05:59:36 +02:00

58 lines
2.1 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { MCPManager } from "@oh-my-pi/pi-coding-agent/mcp/manager";
import type { MCPServerConfig, MCPStdioServerConfig } from "@oh-my-pi/pi-coding-agent/mcp/types";
function expectStdio(config: MCPServerConfig): MCPStdioServerConfig {
if (config.type === "http" || config.type === "sse") throw new Error("expected a stdio config");
return config;
}
describe("stdio env value resolution policy", () => {
it("keeps literal-policy env values byte-for-byte and never executes commands", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-env-policy-"));
const sentinel = path.join(tempDir, "pwned");
try {
const manager = new MCPManager(process.cwd());
const env = {
NAME_OF_AMBIENT: "HOME",
EMPTY: "",
BANG: `!touch ${sentinel}`,
};
const resolved = expectStdio(
await manager.prepareConfig({
type: "stdio",
command: "server",
envPolicy: "literal",
env: { ...env },
}),
);
// Agent Plugins §§4.1/9.2: values are opaque package data — no ambient
// env lookup, no empty-value dropping, no `!command` resolution.
expect(resolved.env).toEqual(env);
// prepareConfig awaits any (buggy) resolution, so a shell `touch` would
// have completed by now — the sentinel must not exist.
expect(fs.existsSync(sentinel)).toBe(false);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("keeps legacy env-name expansion for servers without the literal policy", async () => {
const manager = new MCPManager(process.cwd());
const resolved = expectStdio(
await manager.prepareConfig({
type: "stdio",
command: "server",
env: { LOOKUP: "HOME", EMPTY: "" },
}),
);
// Non-plugin servers keep the existing contract: a value naming an
// ambient variable expands, and empty values are dropped.
expect(resolved.env?.LOOKUP).toBe(process.env.HOME ?? "HOME");
expect(resolved.env?.EMPTY).toBeUndefined();
});
});