Skip to content

Commit f865468

Browse files
skoob13claude
andauthored
feat(agent): add remote MCP servers CLI param for agent-server (#1151)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c3f0cfa commit f865468

5 files changed

Lines changed: 165 additions & 1 deletion

File tree

packages/agent/src/server/agent-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ export class AgentServer {
595595

596596
const sessionResponse = await clientConnection.newSession({
597597
cwd: this.config.repositoryPath,
598-
mcpServers: [],
598+
mcpServers: this.config.mcpServers ?? [],
599599
_meta: {
600600
sessionId: payload.run_id,
601601
taskRunId: payload.run_id,

packages/agent/src/server/bin.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Command } from "commander";
33
import { z } from "zod";
44
import { AgentServer } from "./agent-server.js";
5+
import { mcpServersSchema } from "./schemas.js";
56

67
const envSchema = z.object({
78
JWT_PUBLIC_KEY: z
@@ -45,6 +46,10 @@ program
4546
.requiredOption("--repositoryPath <path>", "Path to the repository")
4647
.requiredOption("--taskId <id>", "Task ID")
4748
.requiredOption("--runId <id>", "Task run ID")
49+
.option(
50+
"--mcpServers <json>",
51+
"MCP servers config as JSON array (ACP McpServer[] format)",
52+
)
4853
.action(async (options) => {
4954
const envResult = envSchema.safeParse(process.env);
5055

@@ -60,6 +65,29 @@ program
6065

6166
const mode = options.mode === "background" ? "background" : "interactive";
6267

68+
let mcpServers: z.infer<typeof mcpServersSchema> | undefined;
69+
if (options.mcpServers) {
70+
let parsed: unknown;
71+
try {
72+
parsed = JSON.parse(options.mcpServers);
73+
} catch {
74+
program.error("--mcpServers must be valid JSON");
75+
return;
76+
}
77+
78+
const result = mcpServersSchema.safeParse(parsed);
79+
if (!result.success) {
80+
const errors = result.error.issues
81+
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
82+
.join("\n");
83+
program.error(
84+
`--mcpServers validation failed (only remote http/sse servers are supported):\n${errors}`,
85+
);
86+
return;
87+
}
88+
mcpServers = result.data;
89+
}
90+
6391
const server = new AgentServer({
6492
port: parseInt(options.port, 10),
6593
jwtPublicKey: env.JWT_PUBLIC_KEY,
@@ -70,6 +98,7 @@ program
7098
mode,
7199
taskId: options.taskId,
72100
runId: options.runId,
101+
mcpServers,
73102
});
74103

75104
process.on("SIGINT", async () => {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, expect, it } from "vitest";
2+
import { mcpServersSchema } from "./schemas.js";
3+
4+
describe("mcpServersSchema", () => {
5+
it("accepts a valid HTTP server", () => {
6+
const result = mcpServersSchema.safeParse([
7+
{
8+
type: "http",
9+
name: "my-server",
10+
url: "https://mcp.example.com",
11+
headers: [{ name: "Authorization", value: "Bearer tok" }],
12+
},
13+
]);
14+
expect(result.success).toBe(true);
15+
expect(result.data).toEqual([
16+
{
17+
type: "http",
18+
name: "my-server",
19+
url: "https://mcp.example.com",
20+
headers: [{ name: "Authorization", value: "Bearer tok" }],
21+
},
22+
]);
23+
});
24+
25+
it("accepts a valid SSE server", () => {
26+
const result = mcpServersSchema.safeParse([
27+
{
28+
type: "sse",
29+
name: "sse-server",
30+
url: "https://sse.example.com/events",
31+
headers: [],
32+
},
33+
]);
34+
expect(result.success).toBe(true);
35+
});
36+
37+
it("defaults headers to empty array when omitted", () => {
38+
const result = mcpServersSchema.safeParse([
39+
{ type: "http", name: "no-headers", url: "https://example.com" },
40+
]);
41+
expect(result.success).toBe(true);
42+
expect(result.data?.[0].headers).toEqual([]);
43+
});
44+
45+
it("accepts multiple servers", () => {
46+
const result = mcpServersSchema.safeParse([
47+
{ type: "http", name: "a", url: "https://a.com" },
48+
{ type: "sse", name: "b", url: "https://b.com" },
49+
]);
50+
expect(result.success).toBe(true);
51+
expect(result.data).toHaveLength(2);
52+
});
53+
54+
it("accepts an empty array", () => {
55+
const result = mcpServersSchema.safeParse([]);
56+
expect(result.success).toBe(true);
57+
expect(result.data).toEqual([]);
58+
});
59+
60+
it("rejects stdio servers", () => {
61+
const result = mcpServersSchema.safeParse([
62+
{
63+
type: "stdio",
64+
name: "local",
65+
command: "/usr/bin/mcp",
66+
args: [],
67+
},
68+
]);
69+
expect(result.success).toBe(false);
70+
});
71+
72+
it("rejects servers with no type", () => {
73+
const result = mcpServersSchema.safeParse([
74+
{ name: "missing-type", url: "https://example.com" },
75+
]);
76+
expect(result.success).toBe(false);
77+
});
78+
79+
it("rejects servers with empty name", () => {
80+
const result = mcpServersSchema.safeParse([
81+
{ type: "http", name: "", url: "https://example.com" },
82+
]);
83+
expect(result.success).toBe(false);
84+
});
85+
86+
it("rejects servers with invalid url", () => {
87+
const result = mcpServersSchema.safeParse([
88+
{ type: "http", name: "bad-url", url: "not-a-url" },
89+
]);
90+
expect(result.success).toBe(false);
91+
});
92+
93+
it("rejects servers with missing url", () => {
94+
const result = mcpServersSchema.safeParse([
95+
{ type: "http", name: "no-url" },
96+
]);
97+
expect(result.success).toBe(false);
98+
});
99+
100+
it("rejects non-array input", () => {
101+
expect(mcpServersSchema.safeParse("not-array").success).toBe(false);
102+
expect(mcpServersSchema.safeParse({}).success).toBe(false);
103+
expect(mcpServersSchema.safeParse(null).success).toBe(false);
104+
});
105+
106+
it("rejects headers with missing fields", () => {
107+
const result = mcpServersSchema.safeParse([
108+
{
109+
type: "http",
110+
name: "bad-headers",
111+
url: "https://example.com",
112+
headers: [{ name: "X-Key" }],
113+
},
114+
]);
115+
expect(result.success).toBe(false);
116+
});
117+
});

packages/agent/src/server/schemas.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import { z } from "zod";
22

3+
const httpHeaderSchema = z.object({
4+
name: z.string(),
5+
value: z.string(),
6+
});
7+
8+
const remoteMcpServerSchema = z.object({
9+
type: z.enum(["http", "sse"]),
10+
name: z.string().min(1, "MCP server name is required"),
11+
url: z.string().url("MCP server url must be a valid URL"),
12+
headers: z.array(httpHeaderSchema).default([]),
13+
});
14+
15+
export const mcpServersSchema = z.array(remoteMcpServerSchema);
16+
17+
export type RemoteMcpServer = z.infer<typeof remoteMcpServerSchema>;
18+
319
export const jsonRpcRequestSchema = z.object({
420
jsonrpc: z.literal("2.0"),
521
method: z.string(),

packages/agent/src/server/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AgentMode } from "../types.js";
2+
import type { RemoteMcpServer } from "./schemas.js";
23

34
export interface AgentServerConfig {
45
port: number;
@@ -11,4 +12,5 @@ export interface AgentServerConfig {
1112
taskId: string;
1213
runId: string;
1314
version?: string;
15+
mcpServers?: RemoteMcpServer[];
1416
}

0 commit comments

Comments
 (0)