Skip to content

[mcp] Optional cwd on stdio MCP servers #776

Description

@ausard

What

Add an optional cwd?: string field to McpServerConfig and pass it
through to spawn in connectStdio, resolved against the agent's
working directory.

Why

Today connectStdio (src/mcp/client.ts:182-186) calls spawn(command, args, { env, stdio }) with no cwd. The child therefore inherits
process.cwd(), which is wrong for any MCP server that needs to operate
on a sub-project of a monorepo: users have to hardcode absolute paths
into args, which then breaks the moment the host CLI is invoked from
a different directory. Carryover from the 2026-06-13 brief; the upstream
change is anomalyco/opencode#30676 (commit 7e7ad377, 2026-06-11, +46 -11).

NOTE: this is NOT a duplicate of closed #314, which only added the
ALEXI_PROJECT_DIR env var. That env var lets the child know the
project dir; this issue makes the child actually run from it (or any
declared sub-directory).

How

  1. Edit src/mcp/config.ts:
    • Extend McpServerConfig (line 10 region) with:
      /**
       * Working directory for the spawned stdio server. Relative paths
       * are resolved against `options.workdir` (or `process.cwd()` if no
       * workdir is supplied to `connect()`). Ignored for non-stdio
       * transports.
       */
      cwd?: string;
  2. Edit src/mcp/client.ts:
    • In connectStdio (around line 161-186), before the spawn call,
      compute:
      const baseDir = options?.workdir ?? process.cwd();
      const resolvedCwd = config.cwd
        ? path.isAbsolute(config.cwd)
          ? config.cwd
          : path.resolve(baseDir, config.cwd)
        : undefined;
    • Pass it through: spawn(config.command, config.args || [], { env, stdio: ['pipe', 'pipe', 'pipe'], cwd: resolvedCwd }).
    • When resolvedCwd is undefined, do NOT pass the field (preserves
      today's behaviour for unconfigured servers).
  3. Add an import for path at the top of client.ts if not already
    present.
  4. Tests in src/mcp/__tests__/client.test.ts (or tests/mcp/):
    • Mock child_process.spawn (or the existing wrapper used in the
      suite) and assert the cwd option received.
    • Case A: config.cwd absolute — passed verbatim.
    • Case B: config.cwd relative + options.workdir provided —
      resolved against options.workdir.
    • Case C: config.cwd relative + options.workdir omitted — resolved
      against process.cwd().
    • Case D: config.cwd undefined — cwd key absent from the spawn
      options object (so we do not regress today's inherit-cwd behaviour).
  5. Update routing-config.example.json ONLY if it contains an MCP
    stdio server example that should now demonstrate cwd. If it does
    not, skip — do not introduce one in this PR.

Files to modify

  • src/mcp/config.ts — add cwd?: string to McpServerConfig.
  • src/mcp/client.ts — resolve and pass cwd to spawn; add path
    import if missing.
  • src/mcp/__tests__/client.test.ts (or tests/mcp/client.test.ts) —
    four new test cases.

Done when

  • McpServerConfig.cwd?: string exists and is documented.
  • spawn in connectStdio receives the resolved cwd when
    configured, and omits it when not configured.
  • All four spawn-cwd test cases pass.
  • npm test, npm run lint, npm run typecheck,
    npm run format:check, npm run build are green.
  • Coverage stays at or above the CI 40% line threshold.
  • No changes to process.env injection (already covered by [mcp] Pass ALEXI_PROJECT_DIR to MCP stdio server child processes #314).

Reference

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions