Summary
McpServerEntry::RemoteHttp (a remote MCP server UAR connects out to) has no mechanism to carry a
per-invocation, per-run credential or context to the server it connects to. The connection is
established once — at initial binding, or lazily on reconnect after a transport loss — and reused for
every subsequent tool call against that server, for the lifetime of the process (or until the next
reconnect). This blocks a real downstream design: a BFF in front of UAR that needs UAR's outbound
/mcp calls to carry the identity of whichever human/tenant started the specific run that triggered the
tool call, so the MCP server on the other end can enforce per-tenant data isolation without trusting an
unverified, caller-supplied argument.
Where this was found
Planning d-server (a downstream consumer project, go-mark/GoMark) — specifically gd03 (an MCP tool
server exposing gomark_domain operations to UAR) and gd04 (gofast_server's BFF in front of UAR).
The design needed UAR, when it calls out to gd03's /mcp endpoint on behalf of a running agent, to
forward some proof of which tenant/human started that run — so gd03's tool handlers could scope their
queries correctly without re-implementing session verification independently (which would duplicate
gofast_server's own auth, and still requires some way to get the verified identity across the wire).
Two designs were tried and both failed against this repository's real code, read directly rather than
assumed:
- A run-scoped bearer token, minted fresh per run, forwarded by UAR as an HTTP header on each
outbound /mcp call. This is impossible with the current schema — see "Root cause" below.
- A static, long-lived service credential (proving "this call is from our own UAR instance") plus a
caller-supplied tenant_id argument on each tool call, trusted by the tool handler. This is
buildable against UAR as-is, but it forces the consuming application (gofast_server) to violate
its own security posture: a blocking constraint in that project reads "Axum derives tenant (team) and
roles from a server-verified session. No handler accepts a caller-selected tenant or trusts unverified
token claims" — and a static-credential-plus-trusted-argument design is exactly the caller-selected-
tenant pattern that rule exists to forbid. An independent adversarial review pass (cross-model, judge
≠ producer) correctly flagged this as a real security regression, not a style nitpick.
Neither design is acceptable. The root cause is upstream, in this repository, not something the
downstream consumer can design around.
Root cause (exact, current source)
src/mcp/config.rs:41-57 — McpServerEntry::RemoteHttp has exactly two fields:
RemoteHttp {
url: String,
#[serde(default)]
env: HashMap<String, String>,
},
env is not used to set outbound HTTP headers. It is only used to expand ${VAR} placeholders that
appear inside the url string itself — confirmed at the call site:
src/mcp/registry.rs:633-641:
McpServerEntry::RemoteHttp { url, env } => {
let env = expand_env_map(env);
let endpoint = resolve_remote_http_url(name, url, &env)?;
().serve(StreamableHttpClientTransport::from_uri(
endpoint.to_string(),
))
.await
.with_context(|| format!("failed to connect remote MCP server '{name}'"))
}
There is no field, anywhere in McpServerEntry, for static or dynamic outbound headers. The connection
itself — StreamableHttpClientTransport::from_uri(endpoint) — is a single, long-lived transport handed
to .serve(), not something re-established or re-parameterized per tool call.
connect_server is called from exactly one call site (src/mcp/registry.rs:2246, inside a
reconnect-after-transport-loss path). Its own neighboring comment makes the current design explicit and
intentional, not an oversight this issue is second-guessing: "A child cannot re-read auth/env/config
after transport loss ... Re-establish the transport for future calls." Config (and therefore any
credential embedded in it) is read once per connection lifetime — at initial bind, or at reconnect — and
is then fixed for every tool call made over that connection until the next reconnect. There is currently
no code path, anywhere in src/mcp/, that re-evaluates McpServerEntry, re-resolves ${VAR} placeholders,
or attaches anything at all to an individual outbound tool-call request beyond what the transport already
carries from bind time.
Conclusion, stated precisely: UAR's RemoteHttp MCP client has no mechanism — none — to carry a
value that varies per agent run or per tool call to a remote MCP server. Everything it can send is fixed
at connection-establishment time and identical for every call made over that connection's lifetime.
Why this matters (the actual downstream requirement)
A BFF in front of UAR (or any application embedding UAR as a library/service and fronting it with its
own auth) has a real, human-verified identity (tenant, role, user) available at the moment it starts an
agent run. UAR itself, once it starts calling tools — including remote MCP servers it doesn't own — has
no way to carry that identity forward to those tool calls, because:
- UAR's own inbound API today has no per-caller auth on top of the run-start call itself in the
relevant configuration (confirmed separately, in this same downstream project's gc04 change: UAR's
API surface relies on compose-network isolation, not per-request identity verification, in its
current deployment shape).
- Even if UAR's inbound API did verify the caller, nothing in
src/mcp/ today propagates that
verified identity onward into the RemoteHttp transport's outbound calls.
The result: any remote MCP server UAR calls on an application's behalf can prove "this request came from
some UAR instance holding my static credential," but never "this request is acting on behalf of tenant
X, verified by the application that started this run." Any application wanting real per-tenant
authorization at the MCP-tool layer is currently forced to choose between (a) trusting an unverified
argument (a real security regression), or (b) reimplementing its own, entirely separate, out-of-band
identity-verification path at the tool server that has nothing to do with the run UAR is actually
executing — which is fragile and easy to get wrong independently.
Proposed fix
The shape that fits UAR's existing architecture with the least disruption, without asserting this is the
only possible design:
-
Extend McpServerEntry::RemoteHttp with an optional static headers: HashMap<String, String>
field, expanded through the same expand_env_map/${VAR} mechanism url already uses. This alone
closes the simplest half of the gap (a static service credential can be sent as a real header
instead of smuggled into the URL as a query parameter, which is what today's schema forces and which
leaks into logs/referrers far more readily than a header does). Low risk, additive, no behavior change
for existing configs that don't set it.
-
The harder, real half — per-run/per-call context propagation — needs a new concept, since nothing
in the current binding/connection model has a notion of "the current run" at the point a RemoteHttp
transport is established or reused. Two directions worth scoping, in increasing order of invasiveness:
- (a) A per-run context object, threaded from wherever a run starts (
RunContext or similar,
UAR already has NativeExecutionContext for its own native tools — src/uar/runtime/native_skill.rs
— which is a real precedent for "context threaded into a tool call") extended to also reach
connect_server/the RemoteHttp call path, with an application-supplied callback or field
(e.g., run_context.external_headers()) merged into the outbound request's headers per call,
not baked into the long-lived transport at bind time. This likely requires moving from "one
long-lived transport reused for every call" to either a lighter-weight per-call header injection at
the StreamableHttpClientTransport layer (if the underlying rmcp crate's HTTP client supports
per-request header overrides — needs checking against rmcp's own API) or reconnecting per run
(likely too expensive for high-frequency tool calls).
- (b) A registration-time, per-agent (not per-run) scoped credential — coarser-grained than (a)
but far less invasive: when an agent is registered (as in this downstream project's gc04), the
RemoteHttp entry's credential could be scoped to that agent's own identity rather than a single
global static secret shared by every agent and every run. This does not solve per-run/per-tenant
scoping, but it at least narrows the blast radius of the single-static-credential design from "any
agent, any run, any tenant" to "this specific agent." Worth doing even if (a) is deferred, since it's
a real improvement in isolation with much less design risk.
Recommend scoping (1) and (2b) as a near-term, low-risk change, and (2a) as its own, carefully-designed
follow-up — it changes the shape of connect_server's call graph and needs its own investigation into
whether rmcp's StreamableHttpClientTransport (or whatever transport crate UAR uses) supports
per-request header overrides at all before committing to a design.
What is NOT being claimed
- This is not a claim that UAR's current design is wrong for its primary use case (a human/CLI-driven
agent session with tools UAR itself governs). It is a real gap specifically for the "application fronts
UAR with its own verified auth and needs that identity to reach a remote, application-owned MCP
server" use case, which may not have been a design target originally.
- This is not a request to weaken UAR's own Cedar-based tool governance — that governance decides
Permit/Deny/RequireApproval for tools UAR itself knows about, and is orthogonal to this issue. This
issue is about identity propagating to an external MCP server UAR calls, not about UAR's own
authorization decision for calling it.
- No code in this issue has been written or proposed as a patch — this is an analysis and a design
sketch for the owner to evaluate, scope, and prioritize.
Summary
McpServerEntry::RemoteHttp(a remote MCP server UAR connects out to) has no mechanism to carry aper-invocation, per-run credential or context to the server it connects to. The connection is
established once — at initial binding, or lazily on reconnect after a transport loss — and reused for
every subsequent tool call against that server, for the lifetime of the process (or until the next
reconnect). This blocks a real downstream design: a BFF in front of UAR that needs UAR's outbound
/mcpcalls to carry the identity of whichever human/tenant started the specific run that triggered thetool call, so the MCP server on the other end can enforce per-tenant data isolation without trusting an
unverified, caller-supplied argument.
Where this was found
Planning
d-server(a downstream consumer project, go-mark/GoMark) — specificallygd03(an MCP toolserver exposing
gomark_domainoperations to UAR) andgd04(gofast_server's BFF in front of UAR).The design needed UAR, when it calls out to
gd03's/mcpendpoint on behalf of a running agent, toforward some proof of which tenant/human started that run — so
gd03's tool handlers could scope theirqueries correctly without re-implementing session verification independently (which would duplicate
gofast_server's own auth, and still requires some way to get the verified identity across the wire).Two designs were tried and both failed against this repository's real code, read directly rather than
assumed:
outbound
/mcpcall. This is impossible with the current schema — see "Root cause" below.caller-supplied
tenant_idargument on each tool call, trusted by the tool handler. This isbuildable against UAR as-is, but it forces the consuming application (
gofast_server) to violateits own security posture: a blocking constraint in that project reads "Axum derives tenant (team) and
roles from a server-verified session. No handler accepts a caller-selected tenant or trusts unverified
token claims" — and a static-credential-plus-trusted-argument design is exactly the caller-selected-
tenant pattern that rule exists to forbid. An independent adversarial review pass (cross-model, judge
≠ producer) correctly flagged this as a real security regression, not a style nitpick.
Neither design is acceptable. The root cause is upstream, in this repository, not something the
downstream consumer can design around.
Root cause (exact, current source)
src/mcp/config.rs:41-57—McpServerEntry::RemoteHttphas exactly two fields:envis not used to set outbound HTTP headers. It is only used to expand${VAR}placeholders thatappear inside the
urlstring itself — confirmed at the call site:src/mcp/registry.rs:633-641:There is no field, anywhere in
McpServerEntry, for static or dynamic outbound headers. The connectionitself —
StreamableHttpClientTransport::from_uri(endpoint)— is a single, long-lived transport handedto
.serve(), not something re-established or re-parameterized per tool call.connect_serveris called from exactly one call site (src/mcp/registry.rs:2246, inside areconnect-after-transport-loss path). Its own neighboring comment makes the current design explicit and
intentional, not an oversight this issue is second-guessing: "A child cannot re-read auth/env/config
after transport loss ... Re-establish the transport for future calls." Config (and therefore any
credential embedded in it) is read once per connection lifetime — at initial bind, or at reconnect — and
is then fixed for every tool call made over that connection until the next reconnect. There is currently
no code path, anywhere in
src/mcp/, that re-evaluatesMcpServerEntry, re-resolves${VAR}placeholders,or attaches anything at all to an individual outbound tool-call request beyond what the transport already
carries from bind time.
Conclusion, stated precisely: UAR's
RemoteHttpMCP client has no mechanism — none — to carry avalue that varies per agent run or per tool call to a remote MCP server. Everything it can send is fixed
at connection-establishment time and identical for every call made over that connection's lifetime.
Why this matters (the actual downstream requirement)
A BFF in front of UAR (or any application embedding UAR as a library/service and fronting it with its
own auth) has a real, human-verified identity (tenant, role, user) available at the moment it starts an
agent run. UAR itself, once it starts calling tools — including remote MCP servers it doesn't own — has
no way to carry that identity forward to those tool calls, because:
relevant configuration (confirmed separately, in this same downstream project's
gc04change: UAR'sAPI surface relies on compose-network isolation, not per-request identity verification, in its
current deployment shape).
src/mcp/today propagates thatverified identity onward into the
RemoteHttptransport's outbound calls.The result: any remote MCP server UAR calls on an application's behalf can prove "this request came from
some UAR instance holding my static credential," but never "this request is acting on behalf of tenant
X, verified by the application that started this run." Any application wanting real per-tenant
authorization at the MCP-tool layer is currently forced to choose between (a) trusting an unverified
argument (a real security regression), or (b) reimplementing its own, entirely separate, out-of-band
identity-verification path at the tool server that has nothing to do with the run UAR is actually
executing — which is fragile and easy to get wrong independently.
Proposed fix
The shape that fits UAR's existing architecture with the least disruption, without asserting this is the
only possible design:
Extend
McpServerEntry::RemoteHttpwith an optional staticheaders: HashMap<String, String>field, expanded through the same
expand_env_map/${VAR}mechanismurlalready uses. This alonecloses the simplest half of the gap (a static service credential can be sent as a real header
instead of smuggled into the URL as a query parameter, which is what today's schema forces and which
leaks into logs/referrers far more readily than a header does). Low risk, additive, no behavior change
for existing configs that don't set it.
The harder, real half — per-run/per-call context propagation — needs a new concept, since nothing
in the current binding/connection model has a notion of "the current run" at the point a
RemoteHttptransport is established or reused. Two directions worth scoping, in increasing order of invasiveness:
RunContextor similar,UAR already has
NativeExecutionContextfor its own native tools —src/uar/runtime/native_skill.rs— which is a real precedent for "context threaded into a tool call") extended to also reach
connect_server/theRemoteHttpcall path, with an application-supplied callback or field(e.g.,
run_context.external_headers()) merged into the outbound request's headers per call,not baked into the long-lived transport at bind time. This likely requires moving from "one
long-lived transport reused for every call" to either a lighter-weight per-call header injection at
the
StreamableHttpClientTransportlayer (if the underlyingrmcpcrate's HTTP client supportsper-request header overrides — needs checking against
rmcp's own API) or reconnecting per run(likely too expensive for high-frequency tool calls).
but far less invasive: when an agent is registered (as in this downstream project's
gc04), theRemoteHttpentry's credential could be scoped to that agent's own identity rather than a singleglobal static secret shared by every agent and every run. This does not solve per-run/per-tenant
scoping, but it at least narrows the blast radius of the single-static-credential design from "any
agent, any run, any tenant" to "this specific agent." Worth doing even if (a) is deferred, since it's
a real improvement in isolation with much less design risk.
Recommend scoping (1) and (2b) as a near-term, low-risk change, and (2a) as its own, carefully-designed
follow-up — it changes the shape of
connect_server's call graph and needs its own investigation intowhether
rmcp'sStreamableHttpClientTransport(or whatever transport crate UAR uses) supportsper-request header overrides at all before committing to a design.
What is NOT being claimed
agent session with tools UAR itself governs). It is a real gap specifically for the "application fronts
UAR with its own verified auth and needs that identity to reach a remote, application-owned MCP
server" use case, which may not have been a design target originally.
Permit/Deny/RequireApproval for tools UAR itself knows about, and is orthogonal to this issue. This
issue is about identity propagating to an external MCP server UAR calls, not about UAR's own
authorization decision for calling it.
sketch for the owner to evaluate, scope, and prioritize.