Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/agents/definitions/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const claude: AgentDefinition = {
shared: false,
},
serializeServer(s) {
if (s.url) return httpServer(s);
if (s.url) return httpServer(s, "http");
const env = envRecord(s.env, (k) => `\${${k}}`);
return [s.name, { command: s.command, args: s.args ?? [], ...(env && { env }) }];
},
Expand Down
8 changes: 8 additions & 0 deletions src/agents/definitions/codex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AgentDefinition } from "../types.js";
import { UnsupportedFeature } from "../errors.js";
import claude from "./claude.js";
import { envRecord } from "./helpers.js";

const codex: AgentDefinition = {
...claude,
Expand All @@ -16,6 +17,13 @@ const codex: AgentDefinition = {
format: "toml",
shared: true,
},
serializeServer(s) {
if (s.url) {
return [s.name, { url: s.url, ...(s.headers && { http_headers: s.headers }) }];
}
const env = envRecord(s.env, (k) => `\${${k}}`);
return [s.name, { command: s.command, args: s.args ?? [], ...(env && { env }) }];
},
hooks: undefined,
serializeHooks() {
throw new UnsupportedFeature("codex", "hooks");
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const vscode: AgentDefinition = {
shared: false,
},
serializeServer(s) {
if (s.url) return httpServer(s, "sse");
if (s.url) return httpServer(s, "http");
const env = envRecord(s.env, (k) => `\${input:${k}}`);
return [
s.name,
Expand Down
102 changes: 102 additions & 0 deletions src/agents/mcp-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,108 @@ describe("writeMcpConfigs", () => {
expect(content.mcpServers.remote).toBeDefined();
});

it("writes claude HTTP server with type: http", async () => {
await writeMcpConfigs(["claude"], [HTTP_SERVER], projectMcpResolver(dir));

const content = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8"));
expect(content.mcpServers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
});

it("writes cursor HTTP server with type: http", async () => {
await writeMcpConfigs(["cursor"], [HTTP_SERVER], projectMcpResolver(dir));

const content = JSON.parse(await readFile(join(dir, ".cursor", "mcp.json"), "utf-8"));
expect(content.mcpServers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
});

it("writes vscode HTTP server with type: http", async () => {
await writeMcpConfigs(["vscode"], [HTTP_SERVER], projectMcpResolver(dir));

const content = JSON.parse(await readFile(join(dir, ".vscode", "mcp.json"), "utf-8"));
expect(content.servers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
});

it("writes opencode HTTP server with type: remote", async () => {
await writeMcpConfigs(["opencode"], [HTTP_SERVER], projectMcpResolver(dir));

const content = JSON.parse(await readFile(join(dir, "opencode.json"), "utf-8"));
expect(content.mcp.remote).toEqual({
type: "remote",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
});

it("writes codex HTTP server with http_headers and no type", async () => {
await writeMcpConfigs(["codex"], [HTTP_SERVER], projectMcpResolver(dir));

const { parse: parseTOML } = await import("smol-toml");
const raw = await readFile(join(dir, ".codex", "config.toml"), "utf-8");
const content = parseTOML(raw) as Record<string, Record<string, Record<string, unknown>>>;
expect(content["mcp_servers"]!["remote"]).toEqual({
url: "https://mcp.example.com/sse",
http_headers: { Authorization: "Bearer tok" },
});
});

it("writes correct HTTP servers for all agents", async () => {
const allAgents = ["claude", "cursor", "vscode", "opencode", "codex"];
await writeMcpConfigs(allAgents, [STDIO_SERVER, HTTP_SERVER], projectMcpResolver(dir));

// Claude
const claude = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8"));
expect(claude.mcpServers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});

// Cursor
const cursor = JSON.parse(await readFile(join(dir, ".cursor", "mcp.json"), "utf-8"));
expect(cursor.mcpServers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});

// VS Code
const vscode = JSON.parse(await readFile(join(dir, ".vscode", "mcp.json"), "utf-8"));
expect(vscode.servers.remote).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});

// OpenCode
const opencode = JSON.parse(await readFile(join(dir, "opencode.json"), "utf-8"));
expect(opencode.mcp.remote).toEqual({
type: "remote",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});

// Codex
const { parse: parseTOML } = await import("smol-toml");
const raw = await readFile(join(dir, ".codex", "config.toml"), "utf-8");
const codex = parseTOML(raw) as Record<string, Record<string, Record<string, unknown>>>;
expect(codex["mcp_servers"]!["remote"]).toEqual({
url: "https://mcp.example.com/sse",
http_headers: { Authorization: "Bearer tok" },
});
});

it("merges into existing shared config file", async () => {
// Codex config.toml is shared — write something else first
const codexDir = join(dir, ".codex");
Expand Down
14 changes: 12 additions & 2 deletions src/agents/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe("claude serializer", () => {
const [name, config] = agent.serializeServer(HTTP_SERVER);
expect(name).toBe("remote-api");
expect(config).toEqual({
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
Expand Down Expand Up @@ -90,6 +91,15 @@ describe("codex serializer", () => {
});
});

it("serializes http server with http_headers and no type", () => {
const [name, config] = agent.serializeServer(HTTP_SERVER);
expect(name).toBe("remote-api");
expect(config).toEqual({
url: "https://mcp.example.com/sse",
http_headers: { Authorization: "Bearer tok" },
});
});

it("has toml format and shared flag", () => {
expect(agent.mcp.format).toBe("toml");
expect(agent.mcp.shared).toBe(true);
Expand All @@ -110,11 +120,11 @@ describe("vscode serializer", () => {
});
});

it("serializes http server with sse type", () => {
it("serializes http server with http type", () => {
const [name, config] = agent.serializeServer(HTTP_SERVER);
expect(name).toBe("remote-api");
expect(config).toEqual({
type: "sse",
type: "http",
url: "https://mcp.example.com/sse",
headers: { Authorization: "Bearer tok" },
});
Expand Down