Skip to content

Add SDK MCP OAuth host token handlers#1669

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

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

Conversation

@roji

@roji roji commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add SDK-facing MCP OAuth host-token callbacks across Node, Python, Go, .NET, Java, and Rust.
  • Wire callbacks to the SDK-safe session.mcp.oauth.handlePendingRequest RPC path and cancel on denied/empty/throwing handlers according to existing callback conventions.
  • Thread optional mcp.oauth_required.data.resourceMetadata through public MCP auth request handler models across SDK languages.
  • Preserve optional behavior for metadata and parsed wwwAuthenticateParams when runtimes omit them.
  • Register mcp.oauth_required interest only when the high-level MCP auth callback is configured.
  • Add MCP OAuth host-token E2E coverage for Node, Python, Go, .NET, Java, and Rust with a shared fake OAuth-protected HTTP MCP server fixture.

Notes

  • Rebasing onto latest main brought in @github/copilot 1.0.64-3 and generated protocol surfaces for session.mcp.oauth.handlePendingRequest plus mcp.oauth_required.data.resourceMetadata.
  • The MCP OAuth E2Es validate against the packaged runtime from main; the temporary sibling-runtime override has been removed from the net PR diff.
  • The packaged runtime rejects session.eventLog.registerInterest before session.create for client-generated session IDs, so SDKs now keep local session handler pre-registration for create-time callbacks but register MCP OAuth interest immediately after session.create succeeds.

Validation

  • cd nodejs && npm ci
  • cd test/harness && npm ci
  • cd nodejs && npm run typecheck && npx vitest run test/e2e/mcp_oauth.e2e.test.ts
  • cd go && go test ./...
  • cd python && uv run --extra dev pytest ../python/test_client.py -k mcp_auth && uv run --extra dev pytest ../python/e2e/test_mcp_oauth_e2e.py
  • cd dotnet && DOTNET_ROOT="$HOME/.dotnet" PATH="$HOME/.dotnet:$PATH" dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~McpOAuthE2ETests|FullyQualifiedName~ClientSessionLifetimeTests"
  • cd java && mvn verify
  • cd java && mvn test -Dtest=McpOAuthE2ETest,McpAuthInterestRegistrationTest && mvn spotless:check
  • cd rust && PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rust/bin:$PATH" cargo test --features test-support --test session_test mcp
  • cd rust && PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rust/bin:$PATH" cargo test --features test-support --test e2e should_satisfy_mcp_oauth_using_host_provided_token
  • cd rust && PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rust/bin:$PATH" cargo +nightly-2026-04-14 fmt --check
  • cd rust && PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rust/bin:$PATH" cargo clippy --all-features --all-targets -- -D warnings

@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
Comment on lines +1466 to +1469
.field(
"mcp_auth_handler",
&self.mcp_auth_handler.as_ref().map(|_| "<set>"),
)

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.

Duplicate Debug field — Rust SessionConfig

The mcp_auth_handler field is emitted twice in the Debug implementation (lines 1462–1465 and 1466–1469 are identical). The second block should be removed; otherwise debug/trace output will contain duplicate keys and DebugStruct::finish() may behave unexpectedly with some formatters.

// Remove this duplicate block (lines 1466-1469):
.field(
    "mcp_auth_handler",
    &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
)

Comment thread dotnet/src/Session.cs
Comment on lines +763 to +773
catch (Exception)
{
try
{
await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, new McpOauthPendingRequestResponseCancelled());
}
catch (Exception rpcEx) when (rpcEx is IOException or ObjectDisposedException)
{
// Connection lost or RPC error — nothing we can do.
}
}
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)

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 and others added 6 commits June 24, 2026 12:14
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@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
session.setOpenCanvases(response.openCanvases());

return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null),
return interest.thenCompose(ignored -> updateSessionOptionsForMode(session,
return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class)
return interest
.thenCompose(
ignored -> connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class))
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
&self.mcp_auth_handler.as_ref().map(|_| "<set>"),
)
.field(
"mcp_auth_handler",

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.

🐛 Duplicate Debug fieldmcp_auth_handler is emitted twice here. The second .field("mcp_auth_handler", ...) block (lines 1800–1803) is an exact copy of the one immediately above it (lines 1796–1799). When {:?} is printed, the field will appear twice in the output. One of these blocks should be removed.

Add MCP OAuth host-token E2E coverage for Python, Go, .NET, Java, and Rust using the shared OAuth-protected MCP test server. Align create-session MCP OAuth interest registration with the packaged runtime by registering interest after session.create succeeds while preserving pre-registration of local session handlers for create-time callbacks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
private record OAuthMcpServer(Process process, String url) implements AutoCloseable {
static OAuthMcpServer start(Path repoRoot) throws Exception {
var script = repoRoot.resolve("test").resolve("harness").resolve("test-mcp-oauth-server.mjs");
var processBuilder = new ProcessBuilder("node", script.toString());
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

Overall this PR does an excellent job of implementing MCP OAuth host-token callbacks uniformly across all six SDKs (Node.js, Python, Go, .NET, Java, Rust). The handler registration pattern, session.eventLog.registerInterest wiring, and cancellation-on-error semantics are consistent.

One inconsistency found

.NET McpAuthContext is missing RequestId (inline comment left on dotnet/src/Types.cs).

All other SDKs expose the requestId to the handler:

SDK Where requestId is exposed
Node.js McpAuthRequest.requestId
Python McpAuthRequest["requestId"] (TypedDict key)
Go MCPAuthRequest.RequestID
Java McpAuthRequest.requestId() (record component)
Rust request_id: RequestId — separate handle() parameter
.NET ❌ not exposed in McpAuthContext

This means a .NET handler cannot identify or correlate the specific OAuth request it's responding to (e.g. for logging or debugging).

Notes on Rust

Rust's McpAuthRequest struct intentionally omits request_id (it is passed as a separate handle() parameter instead). This is consistent with the existing Rust SDK handler pattern used for PermissionHandler and ElicitationHandler, so no change needed there.

Everything else looks consistent ✅

  • Handler naming: onMcpAuthRequest / on_mcp_auth_request / OnMCPAuthRequest / OnMcpAuthRequest / setOnMcpAuthRequest / mcp_auth_handler — all follow language conventions correctly.
  • mcp.oauth_required interest is registered conditionally (only when the handler is set) in all six SDKs.
  • Cancellation-on-throw / cancellation-on-null semantics are uniform.
  • resourceMetadata optional field is threaded through all SDKs.
  • E2E test coverage added for Node.js, Go, Python, .NET, Java, and Rust.

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

@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
public string? ResourceMetadata { get; set; }

/// <summary>Static OAuth client configuration, if the server specifies one.</summary>
public McpOauthRequiredStaticClientConfig? StaticClientConfig { get; set; }

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 suggestion: McpAuthContext is missing a RequestId property.

Every other SDK exposes the request ID to the MCP auth handler:

  • Node.js: McpAuthRequest.requestId
  • Python: McpAuthRequest["requestId"] (TypedDict key)
  • Go: MCPAuthRequest.RequestID
  • Java: McpAuthRequest.requestId() (record component)
  • Rust: request_id: RequestId as a separate handle() parameter

In .NET, a handler receiving an MCP OAuth request has no way to identify or log which specific request it's processing. Consider adding:

/// <summary>Unique request identifier for this MCP OAuth request.</summary>
public string RequestId { get; set; } = string.Empty;

(The existing .NET PermissionInvocation also omits request ID, so there is a consistent internal precedent — but for the new McpAuthContext type this cross-SDK gap is worth revisiting.)

public static async Task<OAuthMcpServer> StartAsync(string expectedToken)
{
var repoRoot = FindRepoRoot();
var script = Path.Combine(repoRoot, "test", "harness", "test-mcp-oauth-server.mjs");
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var candidate = Path.Combine(dir.FullName, "test", "harness", "test-mcp-oauth-server.mjs");
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