Skip to content

Add SDK MCP OAuth host token handlers#1669

Draft
roji wants to merge 4 commits into
mainfrom
roji/roji-mcp-auth-sdk
Draft

Add SDK MCP OAuth host token handlers#1669
roji wants to merge 4 commits into
mainfrom
roji/roji-mcp-auth-sdk

Conversation

@roji

@roji roji commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Sync SDK generated RPC/session-event surfaces to the runtime MCP OAuth lifecycle contract: required reason, optional wwwAuthenticateParams.resourceMetadataUrl, optional raw resourceMetadata, and no host refreshToken in token responses.
  • Thread MCP OAuth auth request handling through Node, Python, Go, .NET, Java, and Rust public callback APIs.
  • Preserve event-interest behavior: SDKs register mcp.oauth_required interest only when the high-level MCP auth callback is configured.
  • Expand shared OAuth-protected MCP fixture and per-language E2E coverage for host-delegated initial auth, refresh/replacement, upscope, reauth, cancellation, and structured challenge propagation (scope, resourceMetadataUrl, error=insufficient_scope, and refresh error=invalid_token).

Notes

  • A separate temporary commit (Use local runtime for MCP OAuth E2E validation) points E2E harnesses at /Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js for runtime PR #11277 validation. This should be removed before final merge.
  • The deterministic lifecycle E2E path now covers initial -> refresh -> upscope -> refresh-cancel -> reauth across all supported SDK languages.
  • The prior Node session.mcp.reloadWithConfig workaround was removed; Node E2E passes without it against the current runtime branch.

Validation

  • cd test/harness && npm ci
  • cd nodejs && npm run typecheck && npx vitest run test/e2e/mcp_oauth.e2e.test.ts
  • cd python && uv run pytest e2e/test_mcp_oauth_e2e.py
  • cd go && go test ./internal/e2e -run TestMCPOAuthE2E -count=1
  • cd dotnet && DOTNET_ROLL_FORWARD=Major dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter FullyQualifiedName~McpOAuthE2ETests
  • cd rust && export PATH="$HOME/.cargo/bin:$PATH" && cargo test --features test-support --test e2e mcp_oauth -- --nocapture
  • cd java && mvn test -Dtest=McpOAuthE2ETest
  • Formatting: gofmt -w internal/e2e/mcp_oauth_e2e_test.go, cargo fmt -- tests/e2e/mcp_oauth.rs, and mvn spotless:apply -Dspotless.check.skip=false.

@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Jun 14, 2026
Comment thread python/copilot/client.py Fixed
Comment thread python/copilot/generated/rpc.py Fixed
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.1M

Comment thread rust/src/types.rs Outdated
Comment thread dotnet/src/Session.cs Fixed
Comment thread nodejs/src/types.ts
Comment on lines +1559 to +1565
export interface McpAuthStaticClientConfig {
/** OAuth client ID for the server. */
clientId: string;
/** Optional non-default OAuth grant type. */
grantType?: "client_credentials";
/** Whether this is a public OAuth client. */
publicClient?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the clientSecret? A lot of AS's use client secrets even for public clients. (VS Code allows specifying a client secret, stored in secret storage)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will add.

Comment thread nodejs/src/types.ts
Comment on lines +1585 to +1594
export interface McpAuthToken {
/** Access token acquired by the SDK host. */
accessToken: string;
/** OAuth token type. Defaults to Bearer when omitted. */
tokenType?: string;
/** Refresh token supplied by the host, if available. */
refreshToken?: string;
/** Token lifetime in seconds, if known. */
expiresIn?: number;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about an idToken? idTokens are useful as they contain claims that are good to display like usernames/email/etc.

@roji roji Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So unless I'm mistaken, the runtime wouldn't have any actual use for it; all it needs as a response from the host is the access token to be forwarded back to the MCP server for access... In fact, I'm about to remove the refreshToken from here, since as discussed in yesterday's meeting, refresh would be the host's responsibility, and so the runtime has no use for that.

But let me know if I have the wrong mental model here! Plus we can always add it later.

@roji roji force-pushed the roji/roji-mcp-auth-sdk branch from 59694fd to 6d3d655 Compare June 24, 2026 10:28
Comment thread java/src/main/java/com/github/copilot/CopilotClient.java Fixed
Comment thread java/src/main/java/com/github/copilot/CopilotClient.java Fixed
Comment thread java/src/main/java/com/github/copilot/CopilotClient.java Fixed
Comment thread java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java Fixed
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 1.8M

Comment thread rust/src/types.rs Outdated
Comment thread java/src/test/java/com/github/copilot/McpOAuthE2ETest.java Fixed
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 3.1M

Comment thread dotnet/src/Types.cs
Comment thread dotnet/test/E2E/McpOAuthE2ETests.cs Fixed
Comment thread dotnet/test/E2E/McpOAuthE2ETests.cs Fixed
@roji roji force-pushed the roji/roji-mcp-auth-sdk branch from a4cfe07 to e3aae80 Compare June 26, 2026 15:18
@github-actions

This comment has been minimized.

Expose host-delegated MCP OAuth handling across SDK languages, sync generated RPC and event models to the lifecycle contract, and add cross-language E2E coverage for initial auth, refresh, upscope, reauth, and cancellation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@roji roji force-pushed the roji/roji-mcp-auth-sdk branch from 530ce4b to a16180e Compare June 26, 2026 15:47
github-actions Bot and others added 3 commits June 26, 2026 15:47
Auto-committed by java-codegen-check workflow.
The java-codegen-check auto-commit dropped generated event/RPC types that the MCP OAuth SDK code still references. Restore them and keep the static client secret event field so the Java SDK compiles and the MCP OAuth E2Es pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Auto-committed by java-codegen-check workflow.
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

This PR is an impressively thorough implementation touching all six SDK languages simultaneously. The core MCP OAuth handler feature is consistently threaded through Node.js, Python, Go, .NET, Java, and Rust with appropriate language idioms. Below are a few targeted observations on cross-SDK consistency that are worth addressing.

Summary

Language McpAuthHandler API McpAuthResult Notes
Node.js (request, { sessionId }) discriminated union { kind: "token" } | { kind: "cancelled" } Also accepts McpAuthToken shorthand ✓
Python (request, dict[str, str]) TypedDict with kind field Context dict key is undocumented ⚠️
Go (request, MCPAuthInvocation) struct { Kind string; Token * } Kind is a magic string with no constants ⚠️
.NET Func<McpAuthContext, Task<McpAuthResult?>> Class with Cancelled bool + factory methods ✓
Java handle(McpAuthRequest) (includes sessionId) record with factory methods ✓
Rust handle(session_id, request_id, McpAuthRequest) enum with variants ✓

Issues

Three concrete items are flagged as inline comments:

  1. Go MCPAuthResult.Kind — undocumented magic strings (go/types.go): Unlike every other SDK, Go has no factory functions or named constants for the Kind field. Users must know to use "token" or "cancelled" to construct a result. Adding two exported constructor functions (analogous to .NET's McpAuthResult.FromToken()/Cancel() and Java's McpAuthResult.token()/cancelled()) would close this gap.

  2. Go MCPAuthWwwAuthenticateParams — inconsistent nullability (go/types.go): ResourceMetadataURL is *string (nil = absent) but Scope and Error are value string (empty = absent/unknown). All three are optional in the schema; handlers can't distinguish absent from empty for Scope/Error. For consistency, both should be *string.

  3. Python McpAuthHandler context — weakly-typed and mixed-convention (python/copilot/session.py): The second parameter is dict[str, str], which neither documents the key names nor gives type-checkers any help. The key passed at call-site is "session_id" (snake_case) while every field in McpAuthRequest uses camelCase (requestId, serverName, serverUrl, ...). A small TypedDict like class McpAuthContext(TypedDict): session_id: str would fix discoverability, and aligning on a single casing convention would reduce confusion.

Non-issues

  • Rust McpAuthRequest omits request_id: intentional — the handler doesn't need the internal correlation ID; it's passed separately by the SDK for its own use. Cleaner API.
  • reason field types vary (string in Go/Node/Python, generated enum in .NET/Java/Rust): acceptable language-idiomatic differences given that Go and Node/Python use string literals/values as standard practice.
  • Different result encoding across languages (.NET Cancelled bool vs Node discriminated union vs Go struct): all idiomatic for their respective type systems.

Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.7M ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.7M

Comment thread go/types.go

// MCPAuthResult is the result returned by an MCP auth request handler.
type MCPAuthResult struct {
Kind string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency: MCPAuthResult needs factory methods or constants

Every other SDK provides type-safe ways to construct a result:

  • .NET: McpAuthResult.FromToken(token) / McpAuthResult.Cancel()
  • Java: McpAuthResult.token(token) / McpAuthResult.cancelled()
  • Rust: McpAuthResult::Token { ... } / McpAuthResult::Cancelled
  • Node/Python: discriminated union / TypedDict with kind: Literal["token", "cancelled"]

Go's MCPAuthResult.Kind is a plain string with no documented valid values — users must discover the magic strings "token" and "cancelled" from source. Consider adding constructor helpers and/or exported constants:

func MCPAuthResultToken(token *MCPAuthToken) *MCPAuthResult {
    return &MCPAuthResult{Kind: "token", Token: token}
}

func MCPAuthResultCancelled() *MCPAuthResult {
    return &MCPAuthResult{Kind: "cancelled"}
}

Comment thread go/types.go
// MCPAuthWwwAuthenticateParams contains parsed parameters from an MCP server's WWW-Authenticate response.
type MCPAuthWwwAuthenticateParams struct {
ResourceMetadataURL *string `json:"resourceMetadataUrl,omitempty"`
Scope string `json:"scope,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency: inconsistent nullability within MCPAuthWwwAuthenticateParams

ResourceMetadataURL is *string (nil = absent), but Scope and Error are value string (empty string = absent). All three fields are optional in the wire schema. The conversion in session.go assigns an empty string when the raw field is nil:

if d.WwwAuthenticateParams.Scope != nil {
    scope = *d.WwwAuthenticateParams.Scope
}
// else scope stays ""

Handlers can't distinguish "scope was not sent" from "scope was sent as empty". All three fields should be *string for consistency and to faithfully represent optionality — matching Node (scope?: string) and Python (TypedDict, total=False) where all fields are uniformly optional.

Comment thread python/copilot/session.py
expiresIn: int


McpAuthHandler = Callable[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency: McpAuthHandler context parameter is weakly typed and uses mixed conventions

Two issues with the second parameter:

1. Type is undocumented (dict[str, str]): type-checkers and IDE autocomplete give no indication of what key(s) are present. Contrast with:

  • Node.js: context: { sessionId: string } — typed inline object
  • Go: MCPAuthInvocation{SessionID string} — named struct

A minimal TypedDict would fix this:

class McpAuthContext(TypedDict):
    session_id: str

2. Mixed casing convention: the context dict key passed at call-site is "session_id" (snake_case), but every field in McpAuthRequest — the same handler's first argument — uses camelCase (requestId, serverName, serverUrl, ...). A user copying a pattern from McpAuthRequest would expect "sessionId", not "session_id", and would get a KeyError at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants