feat(mcp): support remote MCP servers via Streamable HTTP#360
feat(mcp): support remote MCP servers via Streamable HTTP#360lizhengfeng101 wants to merge 3 commits into
Conversation
Support connecting to remote MCP servers using the Streamable HTTP transport from go-sdk. Users configure remote servers with type=remote, a URL, and optional headers (with environment variable expansion for secrets). Authentication stays outside OCR — users provide tokens via headers, matching CLI tool conventions. Closes #335
|
🔍 OpenCodeReview found 3 issue(s) in this PR.
|
| initCtx, initCancel := context.WithTimeout(ctx, 30*time.Second) | ||
| mc, err := mcp.NewRemoteClient(initCtx, name, serverCfg.URL, serverCfg.Headers, version) | ||
| initCancel() | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to connect to remote MCP server %q: %v\n", name, err) | ||
| continue | ||
| } |
There was a problem hiding this comment.
When a header value contains an env var reference like $AUTH_TOKEN and that variable is unset, os.Expand will produce an empty string. The NewRemoteClient function only prints a warning but still proceeds to connect with the empty header value. This could lead to silent authentication failures or, worse, unauthenticated requests being sent to the remote server.
Consider either:
- Treating empty-after-expansion headers as a fatal error for this server (skip it), or
- At minimum, making the warning more prominent (e.g., ERROR level) so users don't miss it in logs.
This is especially important for Authorization headers where an empty value means the request goes out unauthenticated.
| for k, v := range headers { | ||
| expanded[k] = os.Expand(v, os.Getenv) | ||
| if expanded[k] == "" { | ||
| fmt.Fprintf(os.Stderr, "[ocr] WARNING: MCP server %q header %q expanded to empty value — check your environment variables\n", name, k) | ||
| } | ||
| } |
There was a problem hiding this comment.
When a header value expands to an empty string (e.g., because the referenced environment variable is unset), the code prints a warning but still proceeds to set the empty header on outgoing requests. This can lead to silent authentication failures or protocol errors when connecting to the remote MCP server.
Consider either returning an error or skipping the empty header, rather than sending a request known to have invalid headers.
Suggestion:
| for k, v := range headers { | |
| expanded[k] = os.Expand(v, os.Getenv) | |
| if expanded[k] == "" { | |
| fmt.Fprintf(os.Stderr, "[ocr] WARNING: MCP server %q header %q expanded to empty value — check your environment variables\n", name, k) | |
| } | |
| } | |
| for k, v := range headers { | |
| expanded[k] = os.Expand(v, os.Getenv) | |
| if expanded[k] == "" { | |
| return nil, fmt.Errorf("MCP server %q header %q expanded to empty value — check your environment variables", name, k) | |
| } | |
| } |
| parsed, err := url.Parse(value) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid MCP server URL %q: %w", value, err) | ||
| } | ||
| if parsed.Scheme != "http" && parsed.Scheme != "https" { | ||
| return fmt.Errorf("MCP server URL must use http or https scheme, got %q", parsed.Scheme) | ||
| } |
There was a problem hiding this comment.
The url.Parse function in Go is very permissive and rarely returns an error. For example, url.Parse("http://") succeeds with Host == "", which would pass this validation but is not a usable endpoint. Consider adding a check for parsed.Host == "" to catch URLs that have a valid scheme but no host.
Suggestion:
| parsed, err := url.Parse(value) | |
| if err != nil { | |
| return fmt.Errorf("invalid MCP server URL %q: %w", value, err) | |
| } | |
| if parsed.Scheme != "http" && parsed.Scheme != "https" { | |
| return fmt.Errorf("MCP server URL must use http or https scheme, got %q", parsed.Scheme) | |
| } | |
| parsed, err := url.Parse(value) | |
| if err != nil { | |
| return fmt.Errorf("invalid MCP server URL %q: %w", value, err) | |
| } | |
| if parsed.Scheme != "http" && parsed.Scheme != "https" { | |
| return fmt.Errorf("MCP server URL must use http or https scheme, got %q", parsed.Scheme) | |
| } | |
| if parsed.Host == "" { | |
| return fmt.Errorf("MCP server URL %q must include a host", value) | |
| } |
When calling config via the CLI, |
|
Good catch! This is indeed a common CLI pitfall — the user's shell may expand In practice, this is the same issue every CLI tool faces (
We'll add a note in the documentation to remind users to use single quotes (or edit the config file directly) when header values contain environment variable references. No syntax change needed — |
…ning Add remote transport (Streamable HTTP) documentation to MCP Server sections in all READMEs and CLI help text. Include type/url/headers field descriptions, remote server examples (CLI and JSON config), and a tip about using single quotes to prevent shell expansion.
|
Then, do you think #332 should follow that convention too?
Others (e.g. OpenCode) are often using |
|
Thanks for raising the syntax consistency question — it's a good design consideration. I looked into how OpenCode actually handles this. From their source code and documentation:
So in practice, OpenCode also follows the conventional Regarding consistency with #332's That said, the shell-expansion footgun you raised is a real concern and we've addressed it with documentation tips (single quotes for CLI, or edit config.json directly). Happy to hear if you have further thoughts on this. |
|
Correction to my previous comment — I referenced the wrong OpenCode repository (the archived @cometkim is correct: anomalyco/opencode does use /** Apply {env:VAR} and {file:path} substitutions to config text. */
export async function substitute(input: SubstituteInput) {
let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return (input.env?.[varName] ?? process.env[varName]) || ""
})
// ... also supports {file:path} for reading secrets from files
}So OpenCode supports That said, our position remains to use
The shell-expansion footgun is real, but well-mitigated by documentation (single quotes) and by the fact that most users edit If the project later decides to support richer substitution (like |
- Reject URLs with missing host (e.g. "http://") in config validation - Make empty-after-expansion header values a fatal error instead of a warning - Detect HTTP 401/403 responses and surface clear authentication error messages - Drain response body before closing to preserve connection pool reuse
Summary
Add support for remote (HTTP-based) MCP servers, complementing the existing stdio transport.
MCPServerConfigwithtype,url, andheadersfieldsNewRemoteClientusing go-sdk'sStreamableClientTransport$TOKEN→ runtime value)Configuration example
{ "mcp_servers": { "my-remote": { "type": "remote", "url": "https://mcp.example.com/sse", "headers": { "Authorization": "Bearer $MCP_TOKEN" } } } }Design decisions
StreamableClientTransportfrom go-sdk v1.6.1 (the current MCP spec's recommended HTTP transport)$ENV_VAR/${ENV_VAR}expansion viaos.Expandat connect timeCloses #335
🤖 Generated with Claude Code