MCP Client Authentication: Three Modes with User-Token Passthrough
Context
The McpReactAgent connects out to external MCP servers to call their tools. When a tool performs an action against an external system — creating a Jira ticket, sending a message, updating a record — that action needs to be attributed to the actual user who triggered the agent, not to a shared agent service account. Otherwise the external system's audit trail loses the real actor and its per-user authorization cannot be enforced.
The platform already forwards X-AIHub-* request headers along the agent pipeline (API → NATS → dispatcher → RunContext). The requesting user's token can ride along as an X-AIHub-User-Token header. We needed to decide how an MCP connection chooses what credential to present.
The previous model mixed an auth_mode toggle with a separate optional api_key field. Because api_key was nullable, the form rendered it with an "Enable API key" checkbox stacked on top of the toggle — a confusing nested control with no single source of truth.
Decision Drivers
- Attribution
External actions must be recorded against the real user, not a generic service account. - Not every connection is the same
Some MCP servers need no credentials, some need a shared static key, some need per-user identity. - Fail loud, never mask the actor
A misconfigured connection must fail, not silently fall back to a shared key — a silent fallback would attribute the user's action to the service account.
Decision
McpClientConfig.auth_mode is a single three-way choice, and it is the only auth control on the connection:
- none — no credentials are sent.
- api_key — a static bearer token configured on the connection. The
api_keyfield is shown only in this mode. - user_token — the requesting user's bearer token, forwarded as the MCP call's bearer.
McpAuthResolver reads the user token from the X-AIHub-User-Token header that AgentDispatcher stashes in RunContext. McpClientFactory resolves the bearer per mode and raises if api_key or user_token mode is selected without the corresponding credential — it never falls back to another mode.
user_token mode only works for user-initiated chat-completion runs that carry the header. Scheduled, process-initiated, bot-channel, and agent-to-agent runs forward no user token and fail loudly in this mode.
Consequences
Positive
- External actions are attributed to the actual user, so the external system's audit trail and per-user authorization stay correct.
- One explicit control instead of a toggle plus an optional key — the configuration has a single source of truth.
- Misconfiguration fails loudly and never masks the actor behind a shared key.
Trade-offs
user_tokenmode depends on the client sending anX-AIHub-User-Tokenheader; nothing forwards it automatically yet, so it currently requires a client that sets it explicitly.- The forwarded header is untrusted client input — the MCP server must validate the token itself before acting on it.
- An MCP connection that needs per-user auth cannot be driven by non-interactive runs (scheduled, process, bot).
