From 6105de19524a1b22a5025eeb46c8288c514f1851 Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Thu, 25 Jun 2026 09:41:55 +0800 Subject: [PATCH] fix: expand plugin mcp server placeholders (closes #245) --- src/mcp/runtime/parsePluginMcpServers.ts | 24 ++++++-- .../mcp/runtime/parsePluginMcpServers.test.ts | 58 +++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/mcp/runtime/parsePluginMcpServers.test.ts diff --git a/src/mcp/runtime/parsePluginMcpServers.ts b/src/mcp/runtime/parsePluginMcpServers.ts index 83c4524a..d2906ebe 100644 --- a/src/mcp/runtime/parsePluginMcpServers.ts +++ b/src/mcp/runtime/parsePluginMcpServers.ts @@ -20,6 +20,18 @@ function expandHome(s: string): string { return s; } +function expandString(value: string): string { + return expandHome(value) + .replace(/\$\{env:([^}]+)\}/g, (_match, name: string) => process.env[name] ?? "") + .replace(/\$\{userHome\}/g, process.env.HOME ?? process.env.USERPROFILE ?? homedir()); +} + +function expandStringRecord(record: Record): Record { + return Object.fromEntries( + Object.entries(record).map(([key, value]) => [key, expandString(value)]), + ); +} + export type ParsePluginMcpServersResult = { servers: PilotDeckMcpServerSpec[]; diagnostics: { id: string; message: string }[]; @@ -43,12 +55,12 @@ export function parsePluginMcpServers( servers.push({ id, transport: "stdio", - command: v.command, + command: expandString(v.command), args: Array.isArray(v.args) - ? (v.args.filter((a): a is string => typeof a === "string").map(expandHome)) + ? (v.args.filter((a): a is string => typeof a === "string").map(expandString)) : undefined, - env: isStringRecord(v.env) ? (v.env as Record) : undefined, - cwd: typeof v.cwd === "string" ? v.cwd : undefined, + env: isStringRecord(v.env) ? expandStringRecord(v.env as Record) : undefined, + cwd: typeof v.cwd === "string" ? expandString(v.cwd) : undefined, perSession: v.perSession === true ? true : undefined, }); continue; @@ -58,8 +70,8 @@ export function parsePluginMcpServers( servers.push({ id, transport: "streamable_http", - url, - headers: isStringRecord(v.headers) ? (v.headers as Record) : undefined, + url: expandString(url), + headers: isStringRecord(v.headers) ? expandStringRecord(v.headers as Record) : undefined, }); continue; } diff --git a/tests/mcp/runtime/parsePluginMcpServers.test.ts b/tests/mcp/runtime/parsePluginMcpServers.test.ts new file mode 100644 index 00000000..914d81d8 --- /dev/null +++ b/tests/mcp/runtime/parsePluginMcpServers.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { homedir } from "node:os"; + +import { parsePluginMcpServers } from "../../../src/mcp/runtime/parsePluginMcpServers.js"; + +test("plugin MCP server specs expand env and userHome placeholders", () => { + const previousToken = process.env.PLUGIN_MCP_TOKEN; + process.env.PLUGIN_MCP_TOKEN = "secret-token"; + + try { + const result = parsePluginMcpServers({ + local: { + command: "${userHome}/bin/server", + args: ["--config", "${userHome}/mcp.json", "--missing=${env:MISSING_PLUGIN_MCP_TOKEN}"], + cwd: "${userHome}/work", + env: { + TOKEN: "${env:PLUGIN_MCP_TOKEN}", + HOME_PATH: "${userHome}/data", + }, + }, + remote: { + url: "https://example.test/${env:PLUGIN_MCP_TOKEN}", + headers: { + Authorization: "Bearer ${env:PLUGIN_MCP_TOKEN}", + Root: "${userHome}", + }, + }, + }); + + assert.deepEqual(result.diagnostics, []); + const local = result.servers[0]; + assert.equal(local?.transport, "stdio"); + if (!local || local.transport !== "stdio") return; + assert.equal(local.command, `${homedir()}/bin/server`); + assert.deepEqual(local.args, ["--config", `${homedir()}/mcp.json`, "--missing="]); + assert.equal(local.cwd, `${homedir()}/work`); + assert.deepEqual(local.env, { + TOKEN: "secret-token", + HOME_PATH: `${homedir()}/data`, + }); + + const remote = result.servers[1]; + assert.equal(remote?.transport, "streamable_http"); + if (!remote || remote.transport !== "streamable_http") return; + assert.equal(remote.url, "https://example.test/secret-token"); + assert.deepEqual(remote.headers, { + Authorization: "Bearer secret-token", + Root: homedir(), + }); + } finally { + if (previousToken === undefined) { + delete process.env.PLUGIN_MCP_TOKEN; + } else { + process.env.PLUGIN_MCP_TOKEN = previousToken; + } + } +});