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

> Explore the MCP server's mid-conversation OAuth2 flow. Learn how it handles blocking interactive clients while allowing embed sessions to continue seamlessly. Understand the WeKnora approach to token negotiation.

- Repository: [Tencent/WeKnora](https://github.com/tencent/WeKnora)
- Tags: deep-dive
- Published: 2026-09-12

---

**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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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

```go
// 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

```go
// 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

```go
// 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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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`](https://github.com/Tencent/WeKnora/blob/main/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.