Skip to content
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,17 @@ like you would. LSPs can be added manually like so:

### MCPs

Crush also supports Model Context Protocol (MCP) servers through three
transport types: `stdio` for command-line servers, `http` for HTTP endpoints,
and `sse` for Server-Sent Events. Environment variable expansion is supported
using `$(echo $VAR)` syntax.
Crush also supports Model Context Protocol (MCP) servers through three transport
types: `stdio` for command-line servers, `http` for HTTP endpoints, and `sse`
for Server-Sent Events.

Shell-style value expansion (`$VAR`, `${VAR:-default}`, `$(command)`, quoting,
and nesting (works in `command`, `args`, `env`, `headers`, and `url`, so
file-based secrets like work out of the box, so you can use values like
"$TOKEN"` and `"$(cat /path/to/secret/token)"``. Expansion runs through Crush's
embedded shell, so the same syntax works on all supported systems, including
Windows. Unset variables are an error; use `${VAR:-fallback}` to opt in to
a default.

```json
{
Expand Down
40 changes: 32 additions & 8 deletions internal/agent/tools/mcp/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,35 +447,59 @@ func createTransport(ctx context.Context, m config.MCPConfig, resolver config.Va
if strings.TrimSpace(command) == "" {
return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
}
cmd := exec.CommandContext(ctx, home.Long(command), m.Args...)
cmd.Env = append(os.Environ(), m.ResolvedEnv()...)
args, err := m.ResolvedArgs(resolver)
if err != nil {
return nil, err
}
envs, err := m.ResolvedEnv(resolver)
if err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, home.Long(command), args...)
cmd.Env = append(os.Environ(), envs...)
return &mcp.CommandTransport{
Command: cmd,
}, nil
case config.MCPHttp:
if strings.TrimSpace(m.URL) == "" {
url, err := m.ResolvedURL(resolver)
if err != nil {
return nil, err
}
if strings.TrimSpace(url) == "" {
return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field")
}
headers, err := m.ResolvedHeaders(resolver)
if err != nil {
return nil, err
}
client := &http.Client{
Transport: &headerRoundTripper{
headers: m.ResolvedHeaders(),
headers: headers,
},
}
return &mcp.StreamableClientTransport{
Endpoint: m.URL,
Endpoint: url,
HTTPClient: client,
}, nil
case config.MCPSSE:
if strings.TrimSpace(m.URL) == "" {
url, err := m.ResolvedURL(resolver)
if err != nil {
return nil, err
}
if strings.TrimSpace(url) == "" {
return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
}
headers, err := m.ResolvedHeaders(resolver)
if err != nil {
return nil, err
}
client := &http.Client{
Transport: &headerRoundTripper{
headers: m.ResolvedHeaders(),
headers: headers,
},
}
return &mcp.SSEClientTransport{
Endpoint: m.URL,
Endpoint: url,
HTTPClient: client,
}, nil
default:
Expand Down
Loading
Loading