Skip to content

bug(plugins): plugin mcpServers only expand ~ in args and leave env/userHome placeholders literal #245

Description

@MicroMilo

Summary

Plugin manifest mcpServers do not expand ${env:*} or ${userHome}, while user/project mcp.json does. The same MCP server contract therefore behaves differently depending on whether it is declared by a plugin or by user config.

Why it matters

Plugin authors will naturally reuse the placeholder syntax supported by normal MCP config, for example cwd: "${userHome}/x", env.TOKEN: "${env:TOKEN}", or HTTP headers containing tokens. Plugin MCP servers are merged into the same runtime as user MCP servers, but plugin entries keep these placeholders as literal strings. This can break cwd resolution, stdio environment variables, HTTP URLs, or authentication headers.

Evidence

  • src/mcp/config/loadMcpServerConfig.ts:66 returns user/project mcpServers after expandConfig.
  • src/mcp/config/loadMcpServerConfig.ts:69 to src/mcp/config/loadMcpServerConfig.ts:79 recursively expands strings, arrays, and objects.
  • src/mcp/config/loadMcpServerConfig.ts:82 to src/mcp/config/loadMcpServerConfig.ts:85 expands ${env:*} and ${userHome}.
  • src/extension/plugins/protocol/manifest.ts:11 allows plugin manifests to declare mcpServers.
  • src/extension/plugins/config/parsePluginManifest.ts:19 accepts raw.mcpServers directly without placeholder expansion.
  • src/extension/plugins/runtime/PluginRuntime.ts:85 to src/extension/plugins/runtime/PluginRuntime.ts:86 aggregates plugin mcpServers.
  • src/cli/createLocalGateway.ts:684 to src/cli/createLocalGateway.ts:688 merges plugin MCP servers with user config servers before parsing.
  • src/mcp/runtime/parsePluginMcpServers.ts:16 to src/mcp/runtime/parsePluginMcpServers.ts:20 only defines ~ expansion.
  • src/mcp/runtime/parsePluginMcpServers.ts:47 to src/mcp/runtime/parsePluginMcpServers.ts:51 applies that expansion only to stdio args; env and cwd are preserved as-is.
  • src/mcp/runtime/parsePluginMcpServers.ts:56 to src/mcp/runtime/parsePluginMcpServers.ts:62 preserves HTTP url and headers as-is.
  • src/mcp/client/McpClient.ts:171 to src/mcp/client/McpClient.ts:180 passes the parsed stdio/http spec directly into MCP transports.

Validation

Validation level: dynamic parser reproduction.

Repro approach: call parsePluginMcpServers with plugin MCP entries containing args: ["~/server.js"], cwd: "${userHome}/x", env.TOKEN: "${env:PILOTDECK_TEST_TOKEN}", url: "${env:MCP_URL}", and headers.Authorization: "Bearer ${env:TOKEN}".

Key output: args expanded ~/server.js to the user home path, but cwd, env.TOKEN, url, and headers.Authorization remained literal placeholder strings.

Boundary: this did not start a real MCP server. The gateway merge and McpClient transport construction paths show those literal values would be passed to the actual stdio or HTTP transport.

Expected behavior

Plugin manifest MCP server declarations should support the same placeholder semantics as user/project mcp.json, or the plugin manifest schema should explicitly reject unsupported placeholders before runtime.

Existing coverage checked

Coverage review searched for parsePluginMcpServers, mcpServers ${env, ${userHome}, and streamable_http headers.

Coverage label: not covered. PR #84 covers ${projectRoot} support in the user MCP config loader, not plugin manifest parsing. PR #191 changes plugin storage path handling, not MCP placeholder expansion. Issue #210 concerns Windows MCP config behavior and does not cover this plugin manifest root cause.

Suggested fix

Reuse the user MCP config expansion logic for plugin manifest mcpServers, or extract a shared MCP placeholder expansion helper. At minimum, expand stdio args, cwd, and env, plus streamable HTTP url and headers.

Suggested tests

  • ${env:*} expansion in stdio env.
  • ${userHome} expansion in stdio cwd.
  • ~ expansion remains supported in stdio args.
  • ${env:*} expansion in streamable HTTP url.
  • ${env:*} expansion in streamable HTTP headers.
  • User/project mcp.json and plugin manifest MCP entries resolve placeholders consistently.

Submitted with Codex.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions