feat(mcp): reach external MCP servers over HTTP, and resolve ${VAR} secrets from the environment - #91
Merged
Merged
Conversation
MATE serves its own agents as HTTP MCP servers, but the client could only
speak stdio. A remote server therefore had to be reached by spawning
`npx mcp-remote` as a subprocess: Node on the host, a process per server,
and a translation hop to talk a protocol MATE already implements on the
other side.
A server entry is now HTTP when it carries a `url` and stdio when it
carries `command`/`args`. Streamable HTTP is the default transport, with
`sse` selectable for older servers; `headers` carries auth. If an entry
sets both forms, the url wins and the command is ignored rather than
silently spawning a subprocess nobody asked for.
`timeout` keeps the meaning it already has for stdio — how long a slow
tool may take — so it maps to the read timeout, not the connect timeout,
which is separately settable as `connect_timeout`. A url that is not http
or https is refused rather than handed to the transport.
The three config fields that may arrive parsed or as a JSON string were
handling that inline; headers would have been a fourth copy, so the
parsing moved into one helper.
The dashboard's MCP config is a free JSON textarea, so it needed no new
form — only its placeholder, which taught the mcp-remote workaround as
the default. Its help text also promised `${AUTH_HEADER}` interpolation
for secrets; no such substitution exists anywhere in the codebase, so
that sentence is replaced with what actually happens to a pasted token.
789 tests, OK. Reverting the change fails 9 of the 13 new tests; the 4
that still pass are the stdio regression guards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
The MCP config modal has been telling users to write secrets as
${AUTH_HEADER} while no such substitution existed anywhere in the tree.
Anyone who followed the hint sent the literal thirteen-character string
as their credential and got an authentication failure with nothing to
explain it.
Any string value in a server entry — in headers, url, command, args or
env — may now reference the server environment as ${VAR}, so credentials
stay out of the agents_config row and out of config exports.
An unset variable skips that server and logs which names were missing.
Both alternatives are worse: an empty substitution sends an
unauthenticated request that may quietly succeed with reduced scope, and
leaving the placeholder sends `${VAR}` itself as the credential, which is
the bug being fixed. A variable that is set but empty is a real value,
not a miss, since something deliberately set it.
Interpolation runs after JSON parsing rather than over the raw config
text, so a secret containing a quote cannot corrupt a field that arrived
as a JSON string.
Also renames MCPToolset to McpToolset. ADK 2.3 keeps the old name as a
subclass that warns and delegates, so this is behaviour-identical and
only silences the DeprecationWarning. Remaining mentions elsewhere are
prose in unrelated comments and are left alone.
799 tests, OK. Reverting the interpolation fails 8 of the 10 new tests;
the 2 that still pass assert it leaves literal config and bare $NAME
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MATE serves its own agents as HTTP MCP servers, but the client could only speak stdio. A remote server therefore had to be reached by spawning
npx mcp-remoteas a subprocess — Node on the host, a process per server, and a translation hop to talk a protocol MATE already implements on the other side.Two commits, reviewable separately.
1. HTTP/SSE transport for the MCP client
A server entry is now HTTP when it carries a
url, and stdio when it carriescommand/args. Streamable HTTP is the default transport, withsseselectable for older servers;headerscarries auth.ADK 2.3 already ships
StreamableHTTPConnectionParamsandSseConnectionParams, so this is wiring rather than protocol work.Decisions worth a look:
timeoutkeeps the meaning it already has for stdio — how long a slow tool may take — so it maps to the read timeout, not ADK's 5-second connect timeout. Otherwise every config that raisedtimeoutto 300 for a slow tool would silently have started meaning something else.connect_timeoutis separate.The three config fields that may arrive parsed or as a JSON string were handling that inline;
headerswould have been a fourth copy, so the parsing moved into one helper.2.
${VAR}interpolation, and the deprecated class nameThe MCP config modal has been telling users to write secrets as
${AUTH_HEADER}while no such substitution existed anywhere in the tree. Anyone who followed the hint sent the literal thirteen-character string as their credential and got an authentication failure with nothing to explain it.Any string value in a server entry —
headers,url,command,args,env— may now reference the server environment as${VAR}, so credentials stay out of theagents_configrow and out of config exports.Judgement calls, flagged because they are choices rather than obvious:
${VAR}itself as the credential, which is the bug being fixed. Skipping is the only failure that is loud.Only
${NAME}is a placeholder; a bare$HOMEis left alone.Note recorded in the docs: interpolation reads the server's environment, so anyone who can edit an agent's MCP config can route an environment value into a URL they choose. That is the same trust level agent configuration already carries — an admin can enable
code_executor— so it is not a new exposure, but it is now written down rather than implicit.MCPToolsetis also renamed toMcpToolset. ADK 2.3 keeps the old name as a subclass that only warns and delegates, so this is behaviour-identical and silences the DeprecationWarning. Four remaining mentions are prose in comments in unrelated modules and are left alone.UI and docs
The dashboard's MCP config is a free JSON textarea, so it needed no new form — only its placeholder, which taught the
mcp-remoteworkaround as the default example.documents/MCP_SERVERS.mdgains a transport table, the HTTP field reference, and an interpolation section.Verification
799 tests, OK.
Each change was checked by reverting it and confirming the new tests fail:
$NAMEare untouchedThe stdio path is covered by explicit regression tests — it is what every existing installation is configured with, and it is unchanged.
Not covered: no test talks to a real MCP server over the network. The self-referential end-to-end check — pointing MATE at its own
/agents/{name}/mcpendpoint — is worth doing once against a running instance before this is relied on in production.🤖 Generated with Claude Code
https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
Generated by Claude Code