Skip to content

Commit 628ba1e

Browse files
author
OpenClaw Bot
committed
fix(acpx): pass MCP server configuration to ACP sessions
Fixes #39225
1 parent 99cfd27 commit 628ba1e

File tree

5 files changed

+334
-3
lines changed

5 files changed

+334
-3
lines changed

extensions/acpx/openclaw.plugin.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@
3434
"queueOwnerTtlSeconds": {
3535
"type": "number",
3636
"minimum": 0
37+
},
38+
"mcpServers": {
39+
"type": "object",
40+
"additionalProperties": {
41+
"type": "object",
42+
"properties": {
43+
"command": {
44+
"type": "string",
45+
"description": "Command to run the MCP server"
46+
},
47+
"args": {
48+
"type": "array",
49+
"items": { "type": "string" },
50+
"description": "Arguments to pass to the command"
51+
},
52+
"env": {
53+
"type": "object",
54+
"additionalProperties": { "type": "string" },
55+
"description": "Environment variables for the MCP server"
56+
}
57+
},
58+
"required": ["command"]
59+
}
3760
}
3861
}
3962
},
@@ -72,6 +95,11 @@
7295
"label": "Queue Owner TTL Seconds",
7396
"help": "Idle queue-owner TTL for acpx prompt turns. Keep this short in OpenClaw to avoid delayed completion after each turn.",
7497
"advanced": true
98+
},
99+
"mcpServers": {
100+
"label": "MCP Servers",
101+
"help": "MCP server configurations to make available to ACP sessions. Each entry should have a command and optional args and env.",
102+
"advanced": true
75103
}
76104
}
77105
}

extensions/acpx/src/config.test.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe("acpx plugin config parsing", () => {
2121
expect(resolved.allowPluginLocalInstall).toBe(true);
2222
expect(resolved.cwd).toBe(path.resolve("/tmp/workspace"));
2323
expect(resolved.strictWindowsCmdWrapper).toBe(true);
24+
expect(resolved.mcpServers).toEqual({});
2425
});
2526

2627
it("accepts command override and disables plugin-local auto-install", () => {
@@ -133,3 +134,225 @@ describe("acpx plugin config parsing", () => {
133134
).toThrow("strictWindowsCmdWrapper must be a boolean");
134135
});
135136
});
137+
138+
describe("acpx plugin mcpServers config parsing", () => {
139+
it("accepts mcpServers with command-only configuration", () => {
140+
const resolved = resolveAcpxPluginConfig({
141+
rawConfig: {
142+
mcpServers: {
143+
canva: {
144+
command: "npx",
145+
},
146+
},
147+
},
148+
workspaceDir: "/tmp/workspace",
149+
});
150+
151+
expect(resolved.mcpServers).toEqual({
152+
canva: {
153+
command: "npx",
154+
},
155+
});
156+
});
157+
158+
it("accepts mcpServers with command and args", () => {
159+
const resolved = resolveAcpxPluginConfig({
160+
rawConfig: {
161+
mcpServers: {
162+
canva: {
163+
command: "npx",
164+
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
165+
},
166+
},
167+
},
168+
workspaceDir: "/tmp/workspace",
169+
});
170+
171+
expect(resolved.mcpServers).toEqual({
172+
canva: {
173+
command: "npx",
174+
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
175+
},
176+
});
177+
});
178+
179+
it("accepts mcpServers with command, args, and env", () => {
180+
const resolved = resolveAcpxPluginConfig({
181+
rawConfig: {
182+
mcpServers: {
183+
canva: {
184+
command: "npx",
185+
args: ["-y", "mcp-remote@latest"],
186+
env: {
187+
API_KEY: "secret123",
188+
},
189+
},
190+
},
191+
},
192+
workspaceDir: "/tmp/workspace",
193+
});
194+
195+
expect(resolved.mcpServers).toEqual({
196+
canva: {
197+
command: "npx",
198+
args: ["-y", "mcp-remote@latest"],
199+
env: {
200+
API_KEY: "secret123",
201+
},
202+
},
203+
});
204+
});
205+
206+
it("accepts multiple mcpServers", () => {
207+
const resolved = resolveAcpxPluginConfig({
208+
rawConfig: {
209+
mcpServers: {
210+
canva: {
211+
command: "npx",
212+
args: ["mcp-remote@latest", "https://mcp.canva.com/mcp"],
213+
},
214+
github: {
215+
command: "npx",
216+
args: ["-y", "@github/mcp-server"],
217+
env: {
218+
GITHUB_TOKEN: "token123",
219+
},
220+
},
221+
},
222+
},
223+
workspaceDir: "/tmp/workspace",
224+
});
225+
226+
expect(Object.keys(resolved.mcpServers)).toHaveLength(2);
227+
expect(resolved.mcpServers.canva).toBeDefined();
228+
expect(resolved.mcpServers.github).toBeDefined();
229+
});
230+
231+
it("rejects mcpServers with missing command", () => {
232+
expect(() =>
233+
resolveAcpxPluginConfig({
234+
rawConfig: {
235+
mcpServers: {
236+
canva: {
237+
args: ["-y"],
238+
},
239+
},
240+
},
241+
workspaceDir: "/tmp/workspace",
242+
}),
243+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
244+
});
245+
246+
it("rejects mcpServers with non-string command", () => {
247+
expect(() =>
248+
resolveAcpxPluginConfig({
249+
rawConfig: {
250+
mcpServers: {
251+
canva: {
252+
command: 123,
253+
},
254+
},
255+
},
256+
workspaceDir: "/tmp/workspace",
257+
}),
258+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
259+
});
260+
261+
it("rejects mcpServers with non-array args", () => {
262+
expect(() =>
263+
resolveAcpxPluginConfig({
264+
rawConfig: {
265+
mcpServers: {
266+
canva: {
267+
command: "npx",
268+
args: "-y",
269+
},
270+
},
271+
},
272+
workspaceDir: "/tmp/workspace",
273+
}),
274+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
275+
});
276+
277+
it("rejects mcpServers with non-string args items", () => {
278+
expect(() =>
279+
resolveAcpxPluginConfig({
280+
rawConfig: {
281+
mcpServers: {
282+
canva: {
283+
command: "npx",
284+
args: ["-y", 123],
285+
},
286+
},
287+
},
288+
workspaceDir: "/tmp/workspace",
289+
}),
290+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
291+
});
292+
293+
it("rejects mcpServers with non-object env", () => {
294+
expect(() =>
295+
resolveAcpxPluginConfig({
296+
rawConfig: {
297+
mcpServers: {
298+
canva: {
299+
command: "npx",
300+
env: "API_KEY=secret",
301+
},
302+
},
303+
},
304+
workspaceDir: "/tmp/workspace",
305+
}),
306+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
307+
});
308+
309+
it("rejects mcpServers with non-string env values", () => {
310+
expect(() =>
311+
resolveAcpxPluginConfig({
312+
rawConfig: {
313+
mcpServers: {
314+
canva: {
315+
command: "npx",
316+
env: {
317+
API_KEY: 123,
318+
},
319+
},
320+
},
321+
},
322+
workspaceDir: "/tmp/workspace",
323+
}),
324+
).toThrow("mcpServers.canva must have a command string, optional args array, and optional env object");
325+
});
326+
327+
it("schema accepts valid mcpServers config", () => {
328+
const schema = createAcpxPluginConfigSchema();
329+
if (!schema.safeParse) {
330+
throw new Error("acpx config schema missing safeParse");
331+
}
332+
const parsed = schema.safeParse({
333+
mcpServers: {
334+
canva: {
335+
command: "npx",
336+
args: ["-y", "mcp-remote@latest"],
337+
env: {
338+
API_KEY: "secret",
339+
},
340+
},
341+
},
342+
});
343+
344+
expect(parsed.success).toBe(true);
345+
});
346+
347+
it("schema rejects mcpServers with invalid structure", () => {
348+
const schema = createAcpxPluginConfigSchema();
349+
if (!schema.safeParse) {
350+
throw new Error("acpx config schema missing safeParse");
351+
}
352+
const parsed = schema.safeParse({
353+
mcpServers: "invalid",
354+
});
355+
356+
expect(parsed.success).toBe(false);
357+
});
358+
});

0 commit comments

Comments
 (0)