Skip to content

fix(config): restore full shell expansion in MCP config values#2782

Merged
meowgorithm merged 8 commits into
mainfrom
resolve-env-vars
May 10, 2026
Merged

fix(config): restore full shell expansion in MCP config values#2782
meowgorithm merged 8 commits into
mainfrom
resolve-env-vars

Conversation

@meowgorithm

@meowgorithm meowgorithm commented May 2, 2026

Copy link
Copy Markdown
Member

This fixes shell expansion for MCPs, and drastically improves upon the former method.

Basically, shell expansion in config for MCPs broke. When it was working it was home-rolled. It didn't support ${VAR:-default}, mishandled quoted parentheses in $(...), and rejected other constructs the rest of Crush (the bash tool, hooks) already accepts.

Now, MCP config uses the same built-in shell interpreter that Crush uses for the bash tool and hooks, so anything that works there now works in MCP config.

This means you can do things like "$(cat /path/to/secret/token)" and "$(vault kv get my/secret/token)".

Also, now when MCP config is not working due to a missing value, now you'll see it.

Fixes #2334.

Groundwork for replacing the hand-rolled parser in internal/config/resolve.go.

Introduces shell.ExpandValue, a single-value expansion entry point built
on mvdan.cc/sh/v3's syntax + expand packages. Runs with nounset on and
globbing off, preserves internal whitespace, strips only trailing
newlines from command substitution output, and bounds/scrubs inner
stderr surfaced in errors. Shares the builtin/block/coreutils handler
chain with NewShell but uses the caller-provided env verbatim.
Rewires shellVariableResolver onto the embedded shell interpreter used
by the bash tool and hooks, replacing the hand-rolled parser. Adds a
bounded, scrubbed sanitizeResolveError so resolution failures surface
safely without leaking oversized or non-printable inner stderr.
…s/args

Adds MCPConfig.ResolvedArgs and changes ResolvedEnv/ResolvedHeaders to
take a VariableResolver, so createTransport threads one resolver through
command, args, env, and headers. Client mode (IdentityResolver) now
keeps all four fields literal for server-side expansion; server mode
expands uniformly via the shell resolver.
m.URL now runs through the same resolver as command, args, env, and
headers so $VAR / $(cmd) work in http and sse endpoints. The empty-url
guard runs after resolution so ${X:-} still fails cleanly, and failing
expansions surface via the existing StateError path.
Windows t.TempDir() returns C:\... paths whose backslashes get
consumed as escapes when injected into $(cat ...). Convert to
forward slashes so the embedded shell resolves the path on every OS.

Also accept the Windows coreutils error shape ("cannot find") in
addition to POSIX "exit status" when asserting that inner diagnostic
detail is surfaced — mvdan/sh runs coreutils in-process on Windows
so there is no subprocess exit status to report.
@meowgorithm meowgorithm merged commit 2814e40 into main May 10, 2026
15 checks passed
@meowgorithm meowgorithm deleted the resolve-env-vars branch May 10, 2026 00:17
meowgorithm added a commit that referenced this pull request May 13, 2026
fix(config): restore full shell expansion in MCP config values
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

regression(mcp): support file-based secrets in env config (restore shell-expansion behavior)

3 participants