Skip to content

stdio MCP subprocesses receive the full StepCode process environment instead of the SDK safe default #191

Description

@quifox

What happened?

StepCode currently passes the complete StepCode process environment to every local stdio MCP subprocess.

On current main, resolveStepMcpEnvironment() starts by copying every entry from process.env, overlays the MCP server's declared env, and may additionally fill STEPFUN_API_KEY from the stored Step login credential:

export function resolveStepMcpEnvironment(
    declared: Record<string, string> | undefined,
    input: { env?: NodeJS.ProcessEnv; authPath?: string } = {},
): Record<string, string> {
    const resolved: Record<string, string> = {};

    for (const [key, value] of Object.entries(input.env ?? process.env)) {
        if (value !== undefined) resolved[key] = value;
    }

    Object.assign(resolved, declared ?? {});

    if (!resolved.STEPFUN_API_KEY?.trim()) {
        const credential = readStoredCredential(
            "step",
            input.authPath ?? getStepAuthPath(),
        );

        if (
            credential?.type === "oauth" &&
            typeof credential.access === "string" &&
            credential.access.trim()
        ) {
            resolved.STEPFUN_API_KEY = credential.access;
        }

        if (
            credential?.type === "api_key" &&
            typeof credential.key === "string" &&
            credential.key.trim()
        ) {
            resolved.STEPFUN_API_KEY = credential.key;
        }
    }

    return resolved;
}

That object is then explicitly supplied to the MCP SDK transport:

if (input.declaration.command) {
    const env = resolveStepMcpEnvironment(input.declaration.env);

    transport = new StdioClientTransport({
        command: input.declaration.command,
        args: input.declaration.args,
        cwd: input.declaration.cwd,
        env,
        stderr: "pipe",
    });
}

I reproduced this behavior locally with a synthetic sentinel environment variable: a stdio MCP subprocess could read a parent-process variable that had never been declared in that MCP's configuration.

This is notable because StepCode currently depends on @modelcontextprotocol/sdk@1.27.1, and that SDK already has a restricted default for stdio subprocess environments.

If env is not supplied, StdioClientTransport uses getDefaultEnvironment(). In v1.27.1, the Unix default is limited to:

HOME
LOGNAME
PATH
SHELL
TERM
USER

rather than copying arbitrary values from process.env.

SDK implementation:

https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/stdio.ts

StepCode therefore replaces the SDK's restricted default by explicitly supplying its complete resolved process environment.

This report is specifically about local stdio MCP subprocesses. Remote url-based MCP servers use StreamableHTTPClientTransport and do not receive the complete process environment through this path.

Other MCP client behavior

For comparison, several other MCP clients also use restricted subprocess environments rather than forwarding the entire parent environment.

OpenAI Codex builds the MCP environment from a small default baseline plus variables explicitly configured for the MCP server:

https://github.com/openai/codex/blob/b15718acfc5e79dff8cc8999eada7c80df9c85cb/codex-rs/rmcp-client/src/utils.rs

OpenClaw normally starts from the MCP SDK's getDefaultEnvironment() and overlays server-specific environment configuration:

https://github.com/openclaw/openclaw/blob/2c9d4817f0f33cbbf3d5707b8e674333339808de/src/agents/mcp-stdio-transport.ts

Hermes Agent explicitly constructs a safe stdio MCP environment and applies server-declared values:

https://github.com/hermes-agent-org/hermes/blob/036cbdfa0a3158454a0a2a7a7388cf70353326b4/tools/mcp_tool.py

OpenCode currently takes the opposite approach and explicitly merges process.env into the local MCP environment:

https://github.com/anomalyco/opencode/blob/0f549842ee746e400b1f72516b0b2e292e267e2c/packages/opencode/src/mcp/index.ts

So restricted inheritance is not universal across MCP clients. The relevant point here is that the MCP SDK used by StepCode already provides a restricted default, while StepCode explicitly overrides it with the full process environment.

Steps to reproduce

Use a synthetic value so the reproduction does not involve any real credential.

Configure a temporary stdio MCP server:

[mcp_servers.env_probe]
command = "node"
args = [
  "-e",
  "require('fs').writeFileSync('/tmp/step-mcp-env-probe', process.env.STEP_MCP_TEST_VALUE ?? 'missing')"
]

Start StepCode with an unrelated parent-process environment variable:

STEP_MCP_TEST_VALUE=sentinel-value step

The probe process may exit immediately and MCP initialization may subsequently fail; that does not affect this reproduction because the environment is available as soon as the subprocess starts.

Inspect the result:

cat /tmp/step-mcp-env-probe

Observed:

sentinel-value

STEP_MCP_TEST_VALUE was never declared in the MCP configuration, but the subprocess received it.

The same code path also means that when no STEPFUN_API_KEY is already present, resolveStepMcpEnvironment() can load the stored Step login credential and add it to the environment passed to a generic stdio MCP subprocess.

Expected behavior

A stdio MCP subprocess should receive:

  1. the SDK's minimal environment required for normal subprocess execution;
  2. environment variables explicitly declared for that MCP server;
  3. product-specific credentials only when that integration explicitly requires them.

Unrelated parent-process variables should not be forwarded implicitly.

A minimal implementation could be:

const env = {
    ...getDefaultEnvironment(),
    ...(input.declaration.env ?? {}),
};

with Step credential injection handled separately and only for integrations that explicitly require STEPFUN_API_KEY.

This would also preserve the MCP SDK's existing cross-platform handling of required environment variables.

Suggested regression coverage:

  • an unrelated parent sentinel variable is not inherited;
  • PATH, HOME, and other SDK-default variables remain available;
  • explicitly declared MCP env values are preserved;
  • explicit values override the default environment where appropriate;
  • a generic stdio MCP does not implicitly receive a Step login credential;
  • integrations explicitly requiring STEPFUN_API_KEY continue to work.

I have isolated the affected code paths and the regression-test cases and can submit a focused PR if this behavior change matches the maintainers' intended policy.

Version

Reproduced against current main. @step-harness/coding-agent: 0.84.4

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/mcpsrc/mcpbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions