How MCP Server Mid-Conversation OAuth2 Flow Works with Embed and MCP Sessions

WeKnora pauses Model Context Protocol (MCP) tool execution mid-conversation to negotiate OAuth2 tokens, blocking interactive MCP clients like Claude Desktop until the user authorizes, while allowing embed sessions to continue via non-blocking frontend events.

WeKnora supports MCP tools that require external OAuth2 provider authentication (such as cloud storage services). When an agent invokes such a tool, the system orchestrates a secure token exchange that adapts its behavior based on the session type—differentiating between interactive MCP client sessions and non-interactive embed widgets—all while maintaining PKCE security and per-principal token isolation.

The Seven-Step Authorization Flow

The MCP server's OAuth2 flow is identical for both embed and MCP client sessions through the initial authorization request, diverging only in how the conversation thread resumes after token acquisition.

1. Tool Invocation and Gate Request

When an agent invokes an MCP tool whose definition includes MCPAuthOAuth, the tool wrapper in internal/agent/tools/mcp_tool.go triggers the approval gate. The gate's RequestOAuthAndWait method receives the service ID and principal, initiating the mid-conversation pause.

In internal/agent/approval/gate.go, the method RequestOAuthAndWait creates an EventMCPOAuthRequired event. For interactive sessions (MCP clients), this blocks the agent's turn until the token is stored. For non-interactive sessions (embed widgets), the request does not block; instead, the event attaches to the reply so the frontend can render an authorization link.

2. Authorization URL Generation

The EventMCPOAuthRequired event contains a URL generated by OAuthManager.StartAuthorization in internal/mcp/oauth_manager.go. This method performs OAuth discovery, dynamic client registration (DCR), and PKCE generation, returning both the authorization URL and a unique attemptID that tracks the authorization attempt.

The handler endpoint in internal/handler/mcp_oauth.go exposes this via MCPOAuthHandler.AuthorizeURL (lines 106–108), which accepts the service ID and redirects the user to the third-party provider.

3. User Authorization and Callback

The user clicks the authorization URL presented either in the embed UI or the MCP client interface. The third-party provider redirects back to WeKnora's callback endpoint at /api/v1/mcp-services/oauth/callback.

The MCPOAuthHandler.Callback method (lines 23–69 in internal/handler/mcp_oauth.go) validates the state and code parameters, then invokes OAuthManager.CompleteAuthorization to exchange the authorization code for access and refresh tokens.

4. Token Storage and Client Cleanup

CompleteAuthorization persists the token pair to storage, associating it with the extracted principal from mcpOAuthPrincipalsFromContext (lines 39–46). It also clears any stale MCP client that was created with now-invalid registration data, ensuring the next tool invocation uses fresh credentials.

5. Conversation Resumption

Interactive MCP clients: The Gate that was waiting on the token receives notification that the token is ready. The agent's turn automatically continues, and the tool executes with the valid OAuth bearer token attached to the principal.

Non-interactive embed sessions: The original message must be re-sent by the user after authorization. The server recognizes the fresh request with a valid token in internal/types/context_helpers.go via the WithMCPOAuthNonInteractive helper (lines 210–218), allowing the tool to run normally without blocking the original turn.

6. Session Management Endpoints

Additional endpoints in internal/handler/mcp_oauth.go provide lifecycle management:

  • Status: Queries token validity and expiration
  • Revoke: Deletes stored tokens
  • CompleteInConversation: Notifies the server that an MCP client UI has finished the OAuth flow
  • SkipInConversation: Allows MCP clients to dismiss the authorization prompt without acquiring a token

Embed vs. MCP Client Session Handling

The primary architectural difference lies in how each session type handles the waiting period during authorization.

Interactive MCP Client Sessions

For clients like Claude Desktop or VS Code Copilot, the approval gate in internal/agent/approval/gate.go emits the EventMCPOAuthRequired event and synchronously blocks the agent's execution thread. The gate waits on an internal channel until OAuthManager notifies it that the token has been stored. This ensures the conversation appears seamless to the user, with the tool executing immediately after authorization without requiring message repetition.

Non-Interactive Embed Sessions

Web-embedded chat widgets use the MCPOAuthNonInteractive flag set via internal/types/context_helpers.go. When this flag is present, RequestOAuthAndWait returns immediately after emitting the event, attaching the authorization URL to the chat response. The user must click the link, complete authorization in a separate browser tab, and then manually resend their original message. The server recognizes the subsequent request as belonging to the same authorization attempt and proceeds with tool execution.

Key Implementation Details

Principal Extraction: Every OAuth operation extracts two identifiers via mcpOAuthPrincipalsFromContext in internal/handler/mcp_oauth.go:

  • tokenPrincipal: The user identity that owns the stored token
  • gateUserID: The identifier used for tool-approval gating logic

Transport Security: According to internal/types/mcp.go, OAuth-enabled MCP services support only SSE and Streamable HTTP transports. The stdio transport is explicitly disabled for security reasons, preventing local process-based token interception.

Dynamic Registration: The OAuthManager handles Dynamic Client Registration (DCR) automatically during StartAuthorization, ensuring each service instance receives unique client credentials without manual configuration.

Practical Code Examples

Initiating Authorization from the Frontend

// POST /api/v1/mcp-services/{id}/oauth/authorize-url
resp, _ := http.Post(
    fmt.Sprintf("%s/api/v1/mcp-services/%s/oauth/authorize-url", baseURL, serviceID),
    "application/json",
    strings.NewReader(`{
        "redirect_uri": "https://myhost/api/v1/mcp-services/oauth/callback",
        "frontend_redirect": "/settings/mcp"
    }`),
)
var out struct {
    Data struct {
        AuthorizationURL     string `json:"authorization_url"`
        AuthorizationAttempt string `json:"authorization_attempt"`
    } `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println("Open this URL:", out.Data.AuthorizationURL)

Implementation: MCPOAuthHandler.AuthorizeURL calls OAuthManager.StartAuthorization (lines 106–108).

Handling the OAuth Callback

The third-party provider redirects to:


https://myhost/api/v1/mcp-services/oauth/callback?state=abc123&code=xyz789

The server processes this via MCPOAuthHandler.Callback, which executes OAuthManager.CompleteAuthorization (lines 54–55) before redirecting the browser to the frontend completion URL.

Agent Tool Implementation

// internal/agent/tools/mcp_tool.go
func (t *MCPTool) Call(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
    // Request OAuth if needed; blocks for MCP clients, returns immediately for embed
    principal, err := t.gate.RequestOAuthAndWait(ctx, t.ServiceID, t.Principal)
    if err != nil {
        return nil, err
    }
    // principal now carries a valid token; proceed with the remote call
    return t.executeWithToken(ctx, args, principal)
}

Checking Token Status

// GET /api/v1/mcp-services/{id}/oauth/status?authorization_attempt=attempt123
resp, _ := http.Get(fmt.Sprintf(
    "%s/api/v1/mcp-services/%s/oauth/status?authorization_attempt=%s",
    baseURL, serviceID, attemptID,
))
var status struct {
    Authorized       bool   `json:"authorized"`
    State            string `json:"state"`
    RefreshAvailable bool   `json:"refresh_available"`
    ExpiresAt        string `json:"expires_at,omitempty"`
}
json.NewDecoder(resp.Body).Decode(&status)

Implementation: MCPOAuthHandler.Status (lines 84–99).

Summary

  • Tencent/WeKnora implements a unified OAuth2 flow for MCP tools that pauses execution mid-conversation to acquire external tokens.
  • The OAuthManager in internal/mcp/oauth_manager.go handles PKCE, dynamic client registration, and token exchange via StartAuthorization and CompleteAuthorization.
  • The approval Gate in internal/agent/approval/gate.go manages conversation flow, emitting EventMCPOAuthRequired and blocking only for interactive MCP clients.
  • Embed sessions use WithMCPOAuthNonInteractive from internal/types/context_helpers.go to avoid blocking, requiring users to resend messages after authorization.
  • MCP clients experience seamless resumption as the gate automatically unblocks when OAuthManager persists the token.
  • Security constraints limit OAuth-enabled services to SSE and Streamable HTTP transports only, excluding stdio per internal/types/mcp.go.

Frequently Asked Questions

What happens if the user closes the authorization window without completing OAuth?

For MCP client sessions, the user can dismiss the prompt via the SkipInConversation endpoint, which unblocks the gate with an error. For embed sessions, the conversation continues normally; the user simply lacks a valid token, and subsequent tool invocations will re-prompt for authorization.

How does WeKnora ensure OAuth tokens are stored securely and isolated per user?

The mcpOAuthPrincipalsFromContext function extracts a tokenPrincipal that uniquely identifies the user, ensuring tokens are persisted in isolated storage buckets. The OAuthManager in internal/mcp/oauth_manager.go uses this principal when calling CompleteAuthorization, binding the access and refresh tokens specifically to that user's identity.

Can the same MCP tool work with both embed widgets and desktop clients like Claude?

Yes. The tool implementation in internal/agent/tools/mcp_tool.go calls gate.RequestOAuthAndWait transparently. The gate automatically detects the session type via the MCPOAuthNonInteractive flag and adjusts its blocking behavior accordingly, allowing identical tool code to function across both embed and desktop environments.

Why is the stdio transport disabled for OAuth-enabled MCP services?

According to the transport definitions in internal/types/mcp.go, only SSE and Streamable HTTP transports are permitted for OAuth-enabled services. The stdio transport is disabled because local subprocess-based communication cannot securely handle the HTTP callback mechanism required for OAuth2 authorization code flows, and lacks the session persistence needed for mid-conversation token negotiation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →