Skip to content

[FEATURE] Native per-user, human-in-the-loop HITL OAuth for MCP servers #2326

Description

@marcellodesales

📋 Prerequisites

📝 Feature Summary

Let end users authenticate to each MCP server with their OWN OAuth identity, interactively from the chat, so tool calls run under the user's own token instead of a single shared/service credential.

❓ Problem Statement / Motivation

Current limitation. A RemoteMCPServer only carries static credentials, and the same
identity is used for tool discovery and for every user's tool calls. kagent has no MCP OAuth
client (no discovery / dynamic client registration / PKCE / token exchange) and no way to
surface an interactive auth challenge into the chat. When a tool requires the caller's own
authorization, the call just fails — the user is never prompted to connect.

Who is affected. Anyone running kagent against MCP servers that are their own OAuth 2.1
authorization servers and enforce per-user authorization/audit (Kubernetes, Vault, GitHub,
internal enterprise tools, etc.). A kagent-issued or forwarded SSO/id-token has the wrong
audience for those servers and is (correctly) rejected. The only per-user signal that even
reaches an MCP today is the forwarded OIDC id-token, and only if the Agent tool opts into
forwarding it — which is not the right credential for a server that issues its own tokens.

Why it's needed.

  • Users expect the "connect your account" experience they already have with Claude/ChatGPT
    connectors: authorize a server once, as themselves, then their tools work.
  • Platform/security owners need each user's MCP calls to carry that user's identity for least
    privilege, per-user authorization, and a correct audit trail. A shared service token is a
    non-starter for sensitive tools.

💡 Proposed Solution

Make per-user MCP authorization a first-class kagent capability:

  1. kagent as an MCP OAuth client. Treat each MCP server as its own OAuth 2.1
    authorization server: support RFC 8414 metadata discovery, RFC 7591 dynamic client
    registration (public clients), and RFC 7636 PKCE (S256). (Today kagent/agentgateway can
    only be a resource server — this is the missing half.)
  2. Per-user token store, keyed by a verified, stable user identity (e.g. OIDC sub),
    with lifetime tied to the granted token's exp and re-auth on expiry. Never shared across
    users.
  3. Human-in-the-loop authorize flow surfaced in chat. When a call needs auth, the user
    gets an Authorize action in the conversation, logs in via the browser, and the
    conversation resumes where it left off (ideally via MCP elicitation / URL-mode, or an
    equivalent typed "needs authorization + here's the link" signal).
  4. Live per-server auth status in the UI — per user: disabled / needs-authorization /
    authorized
    , plus remaining token lifetime and an (re)authorize affordance.
  5. Separate discovery vs. call identity. Discovery/handshake proceed without a user token;
    only tool invocations require the per-user token, so servers still list their tools pre-auth.

Non-obvious requirements we hit building this against kagent (any implementation should
handle these):

  • MCP-server-as-issuer: the .well-known metadata is public/pre-auth; the flow is
    discovery → dynamic client registration → PKCE → code exchange. Token TTL follows the
    granted token's exp.
  • Discovery vs. call gating: let discovery/handshake/transport (initialize,
    tools/list, resources/list, prompts/list, ping, notifications) run pre-auth; gate
    only privileged ops (tools/call, resources/read, prompts/get). Otherwise the server
    drops to "not accepted" and the user never sees the tools.
  • Surfacing "authorize me" to the client is subtle (Streamable-HTTP through a proxy):
    • Return it on HTTP 200 — strict clients treat a 4xx as a transport failure and abort
      the session (the prompt never reaches chat; the user just sees a hang).
    • Echo the JSON-RPC id, or strict SDK clients reject the response with a validation
      error and the call fails opaquely.
    • For a tools/call, model it as a tool result with isError: true carrying the
      authorize URL as text, so the model reliably relays the link to the user (use a
      protocol-level error only for non-tool gated methods).
  • Return the user to the originating conversation after the browser callback.

🔄 Alternatives Considered

  • Static headersFrom / a shared service credential — fine when a shared identity is
    genuinely intended, but breaks per-user authorization, least privilege, and audit.
  • Forwarding the platform SSO/OIDC id-token to the MCP — wrong audience; the MCP spec
    discourages it, and self-issuing MCP servers reject it.
  • agentgateway as a resource server — validates an inbound token but is never the OAuth
    client, so it can't perform the per-user, interactive authorization this needs.
  • An external broker in front of the MCPs — what we built as a proof of concept (see
    Additional Context). It works, but the auth-challenge relay and per-server status really
    want to be native so every kagent user benefits without bespoke infra.

🎯 Affected Service(s)

Multiple services / System-wide

📚 Additional Context

Prior art / reference implementation. We built a working end-to-end proof of this against
kagent — a per-user MCP OAuth broker (RFC 8414 discovery + RFC 7591 dynamic client
registration + RFC 7636 PKCE, a per-user token store, an in-chat Authorize prompt, and
live per-server status + token TTL in a customized UI). Validated on our platform (Vionix):
a user chats → the agent calls a tool → the chat shows an Authorize link → the user logs in
once → for the token's lifetime their calls run under their own MCP token (🟢 connected).
We went from problem to working flow in ~2 days and are happy to share details and compare notes.

[screenshots to be added: the 🟡→🟢 per-server status dots, the in-chat "authorization
required" prompt with the Authorize link, and the per-server auth-status card with token TTL]

Chat screen showing MCP server must be OAuth'ed

Image

Agent message asking the user to authenticate to the selected MCP server from the conversation with the Oauth2 link

Image

Response from the tool with contextual status

Image Image

🙋 Are you willing to contribute?

  • I am willing to submit a PR for this feature

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions