From a52c2fc2fb68e2a3b31f0d6c6fb465e44470e5fe Mon Sep 17 00:00:00 2001 From: Daniel Young Date: Tue, 18 Aug 2026 08:49:10 -0400 Subject: [PATCH 1/3] [mcp] Expand Claude plugin stdio env (#1) Claude marketplace plugins may reference runtime secrets in stdio server environment values. Resolve those placeholders after plugin-root substitution so child processes receive credentials instead of literal template strings. Solves: Stdio MCP credentials remain unexpanded Tests: Claude plugin discovery tests; coding-agent check; live CH auth --- .../coding-agent/src/discovery/claude-plugins.ts | 4 +++- .../test/discovery/claude-plugins.test.ts | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/discovery/claude-plugins.ts b/packages/coding-agent/src/discovery/claude-plugins.ts index 50d4dea38..8c3e8ed4b 100644 --- a/packages/coding-agent/src/discovery/claude-plugins.ts +++ b/packages/coding-agent/src/discovery/claude-plugins.ts @@ -580,7 +580,9 @@ async function loadMCPServers(ctx: LoadContext): Promise> ...(raw.timeout !== undefined && { timeout: raw.timeout }), ...(rooted.command !== undefined && { command: rooted.command }), ...(raw.args !== undefined && { args: substitutePluginRoot(raw.args, root.path) }), - ...(raw.env !== undefined && { env: substitutePluginRoot(raw.env, root.path) }), + ...(raw.env !== undefined && { + env: expandEnvVarsDeep(substitutePluginRoot(raw.env, root.path)), + }), ...(rooted.cwd !== undefined && { cwd: rooted.cwd }), ...(raw.url !== undefined && { url: expandEnvVarsDeep(raw.url) }), ...(raw.headers !== undefined && { headers: expandEnvVarsDeep(raw.headers) }), diff --git a/packages/coding-agent/test/discovery/claude-plugins.test.ts b/packages/coding-agent/test/discovery/claude-plugins.test.ts index aa28719dd..ce99e7fcd 100644 --- a/packages/coding-agent/test/discovery/claude-plugins.test.ts +++ b/packages/coding-agent/test/discovery/claude-plugins.test.ts @@ -527,7 +527,7 @@ describe("listClaudePluginRoots", () => { ); }); - test("expands env placeholders in marketplace plugin MCP url and headers", async () => { + test("expands env placeholders throughout marketplace plugin MCP configuration", async () => { const pluginsDir = path.join(tempDir, ".claude", "plugins"); const pluginPath = path.join(tempDir, "plugins", "context7"); const originalApiKey = process.env.OMP_PLUGIN_MCP_API_KEY; @@ -566,6 +566,14 @@ describe("listClaudePluginRoots", () => { CONTEXT7_API_KEY: envPlaceholder("OMP_PLUGIN_MCP_API_KEY"), }, }, + local: { + type: "stdio", + command: "${CLAUDE_PLUGIN_ROOT}/bin/server", + env: { + API_KEY: envPlaceholder("OMP_PLUGIN_MCP_API_KEY"), + CONFIG_PATH: "${CLAUDE_PLUGIN_ROOT}/config.json", + }, + }, }), ); @@ -577,6 +585,12 @@ describe("listClaudePluginRoots", () => { expect(server?.url).toBe("https://mcp.context7.example/mcp"); expect(server?.headers).toEqual({ CONTEXT7_API_KEY: "ctx7sk-test-key" }); + const localServer = result.all.find(item => item.name === "context7:local"); + expect(localServer?.command).toBe(path.join(pluginPath, "bin", "server")); + expect(localServer?.env).toEqual({ + API_KEY: "ctx7sk-test-key", + CONFIG_PATH: path.join(pluginPath, "config.json"), + }); } finally { if (originalApiKey === undefined) delete process.env.OMP_PLUGIN_MCP_API_KEY; else process.env.OMP_PLUGIN_MCP_API_KEY = originalApiKey; From 1496a139ce76b96bfb948b25e1ee35d8a5184032 Mon Sep 17 00:00:00 2001 From: Daniel Young Date: Tue, 18 Aug 2026 08:50:06 -0400 Subject: [PATCH 2/3] [mcp] Revert plugin env expansion Keep host-specific secret injection in the maintained plugin layer instead of carrying a behavioral divergence in the OMP fork. Solves: Unwanted fork maintenance for MCP injection Tests: Reverts only drycode/oh-my-pi PR #1 --- .../coding-agent/src/discovery/claude-plugins.ts | 4 +--- .../test/discovery/claude-plugins.test.ts | 16 +--------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/coding-agent/src/discovery/claude-plugins.ts b/packages/coding-agent/src/discovery/claude-plugins.ts index 8c3e8ed4b..50d4dea38 100644 --- a/packages/coding-agent/src/discovery/claude-plugins.ts +++ b/packages/coding-agent/src/discovery/claude-plugins.ts @@ -580,9 +580,7 @@ async function loadMCPServers(ctx: LoadContext): Promise> ...(raw.timeout !== undefined && { timeout: raw.timeout }), ...(rooted.command !== undefined && { command: rooted.command }), ...(raw.args !== undefined && { args: substitutePluginRoot(raw.args, root.path) }), - ...(raw.env !== undefined && { - env: expandEnvVarsDeep(substitutePluginRoot(raw.env, root.path)), - }), + ...(raw.env !== undefined && { env: substitutePluginRoot(raw.env, root.path) }), ...(rooted.cwd !== undefined && { cwd: rooted.cwd }), ...(raw.url !== undefined && { url: expandEnvVarsDeep(raw.url) }), ...(raw.headers !== undefined && { headers: expandEnvVarsDeep(raw.headers) }), diff --git a/packages/coding-agent/test/discovery/claude-plugins.test.ts b/packages/coding-agent/test/discovery/claude-plugins.test.ts index ce99e7fcd..aa28719dd 100644 --- a/packages/coding-agent/test/discovery/claude-plugins.test.ts +++ b/packages/coding-agent/test/discovery/claude-plugins.test.ts @@ -527,7 +527,7 @@ describe("listClaudePluginRoots", () => { ); }); - test("expands env placeholders throughout marketplace plugin MCP configuration", async () => { + test("expands env placeholders in marketplace plugin MCP url and headers", async () => { const pluginsDir = path.join(tempDir, ".claude", "plugins"); const pluginPath = path.join(tempDir, "plugins", "context7"); const originalApiKey = process.env.OMP_PLUGIN_MCP_API_KEY; @@ -566,14 +566,6 @@ describe("listClaudePluginRoots", () => { CONTEXT7_API_KEY: envPlaceholder("OMP_PLUGIN_MCP_API_KEY"), }, }, - local: { - type: "stdio", - command: "${CLAUDE_PLUGIN_ROOT}/bin/server", - env: { - API_KEY: envPlaceholder("OMP_PLUGIN_MCP_API_KEY"), - CONFIG_PATH: "${CLAUDE_PLUGIN_ROOT}/config.json", - }, - }, }), ); @@ -585,12 +577,6 @@ describe("listClaudePluginRoots", () => { expect(server?.url).toBe("https://mcp.context7.example/mcp"); expect(server?.headers).toEqual({ CONTEXT7_API_KEY: "ctx7sk-test-key" }); - const localServer = result.all.find(item => item.name === "context7:local"); - expect(localServer?.command).toBe(path.join(pluginPath, "bin", "server")); - expect(localServer?.env).toEqual({ - API_KEY: "ctx7sk-test-key", - CONFIG_PATH: path.join(pluginPath, "config.json"), - }); } finally { if (originalApiKey === undefined) delete process.env.OMP_PLUGIN_MCP_API_KEY; else process.env.OMP_PLUGIN_MCP_API_KEY = originalApiKey; From fb7c2e1a5265f62f62f067643127938b033a0879 Mon Sep 17 00:00:00 2001 From: Daniel Young Date: Tue, 18 Aug 2026 12:14:25 -0400 Subject: [PATCH 3/3] [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 --- .../coding-agent/src/discovery/omp-plugins.ts | 3 +- .../test/discovery/omp-plugins.test.ts | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/discovery/omp-plugins.ts b/packages/coding-agent/src/discovery/omp-plugins.ts index 3e2cce570..f32e055cc 100644 --- a/packages/coding-agent/src/discovery/omp-plugins.ts +++ b/packages/coding-agent/src/discovery/omp-plugins.ts @@ -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> 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)) { diff --git a/packages/coding-agent/test/discovery/omp-plugins.test.ts b/packages/coding-agent/test/discovery/omp-plugins.test.ts index 1d79183e9..d2d870695 100644 --- a/packages/coding-agent/test/discovery/omp-plugins.test.ts +++ b/packages/coding-agent/test/discovery/omp-plugins.test.ts @@ -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; + cwd?: string; + url?: string; + headers?: Record; + 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"),