[mcp] Expand extension package environment

Apply the same recursive placeholder expansion used by native MCP configs before extension-package servers are validated and surfaced. This prevents stdio credentials and remote headers from reaching servers as literal placeholders.\n\nSolves: Extension-package MCP environment expansion\nTests: bun test packages/coding-agent/test/discovery/omp-plugins.test.ts
This commit is contained in:
Daniel Young
2026-08-18 12:14:25 -04:00
parent 480d72ffd2
commit fb7c2e1a52
2 changed files with 65 additions and 1 deletions
@@ -32,6 +32,7 @@ import { legacyProviderAllowed } from "./agent-plugin-format";
import {
buildRuleFromMarkdown,
createSourceMeta,
expandEnvVarsDeep,
loadFilesFromDir,
parseRequestIdFormat,
scanSkillsFromDir,
@@ -312,7 +313,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
logger.warn(`[omp-plugins] Invalid JSON in ${mcpPath}`);
continue;
}
const servers = parsed.mcpServers;
const servers = expandEnvVarsDeep(parsed.mcpServers);
if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
for (const [serverName, serverCfg] of Object.entries(servers)) {
@@ -286,6 +286,69 @@ test(".mcp.json with bare entries (no command/url) records a warning and is skip
expect((result.warnings ?? []).some(w => w.includes('"broken"'))).toBe(true);
});
test(".mcp.json expands environment placeholders recursively", async () => {
const variables = {
OMP_PLUGIN_COMMAND: "expanded-command",
OMP_PLUGIN_ARG: "expanded-arg",
OMP_PLUGIN_ENV: "expanded-env",
OMP_PLUGIN_CWD: path.join(tempDir, "expanded-cwd"),
OMP_PLUGIN_URL: "https://mcp.example.test",
OMP_PLUGIN_HEADER: "expanded-header",
OMP_PLUGIN_CLIENT_ID: "expanded-client-id",
};
const placeholder = (name: string) => `\${${name}}`;
Object.assign(process.env, variables);
try {
writeFile(
path.join(ext, ".mcp.json"),
JSON.stringify({
mcpServers: {
stdio: {
command: placeholder("OMP_PLUGIN_COMMAND"),
args: [placeholder("OMP_PLUGIN_ARG")],
env: { TOKEN: placeholder("OMP_PLUGIN_ENV") },
cwd: placeholder("OMP_PLUGIN_CWD"),
},
http: {
type: "http",
url: placeholder("OMP_PLUGIN_URL"),
headers: { Authorization: `Bearer ${placeholder("OMP_PLUGIN_HEADER")}` },
oauth: { clientId: placeholder("OMP_PLUGIN_CLIENT_ID") },
},
},
}),
);
writeFile(path.join(project, ".omp", "settings.json"), JSON.stringify({ extensions: [ext] }));
const servers = await loadFromPlugin<{
name: string;
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
headers?: Record<string, string>;
oauth?: { clientId?: string };
}>(mcpCapability.id, ctx());
const stdio = servers.find(server => server.name === "stdio");
const http = servers.find(server => server.name === "http");
expect(stdio).toMatchObject({
command: variables.OMP_PLUGIN_COMMAND,
args: [variables.OMP_PLUGIN_ARG],
env: { TOKEN: variables.OMP_PLUGIN_ENV },
cwd: variables.OMP_PLUGIN_CWD,
});
expect(http).toMatchObject({
url: variables.OMP_PLUGIN_URL,
headers: { Authorization: `Bearer ${variables.OMP_PLUGIN_HEADER}` },
oauth: { clientId: variables.OMP_PLUGIN_CLIENT_ID },
});
} finally {
for (const key of Object.keys(variables)) delete process.env[key];
}
});
test("relative path-like command and cwd resolve against the plugin config directory", async () => {
writeFile(
path.join(ext, ".mcp.json"),