Add SDK MCP OAuth host token handlers#1669
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.1M
| 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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Makes sense, will add.
| 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; | ||
| } |
There was a problem hiding this comment.
what about an idToken? idTokens are useful as they contain claims that are good to display like usernames/email/etc.
There was a problem hiding this comment.
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.
59694fd to
6d3d655
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 1.8M
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 3.1M
a4cfe07 to
e3aae80
Compare
This comment has been minimized.
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>
530ce4b to
a16180e
Compare
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.
Cross-SDK Consistency ReviewThis 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
IssuesThree concrete items are flagged as inline comments:
Non-issues
|
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.7M
|
|
||
| // MCPAuthResult is the result returned by an MCP auth request handler. | ||
| type MCPAuthResult struct { | ||
| Kind string |
There was a problem hiding this comment.
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"}
}| // 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"` |
There was a problem hiding this comment.
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.
| expiresIn: int | ||
|
|
||
|
|
||
| McpAuthHandler = Callable[ |
There was a problem hiding this comment.
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: str2. 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.
Summary
reason, optionalwwwAuthenticateParams.resourceMetadataUrl, optional rawresourceMetadata, and no hostrefreshTokenin token responses.mcp.oauth_requiredinterest only when the high-level MCP auth callback is configured.scope,resourceMetadataUrl,error=insufficient_scope, and refresherror=invalid_token).Notes
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.jsfor runtime PR #11277 validation. This should be removed before final merge.initial -> refresh -> upscope -> refresh-cancel -> reauthacross all supported SDK languages.session.mcp.reloadWithConfigworkaround was removed; Node E2E passes without it against the current runtime branch.Validation
cd test/harness && npm cicd nodejs && npm run typecheck && npx vitest run test/e2e/mcp_oauth.e2e.test.tscd python && uv run pytest e2e/test_mcp_oauth_e2e.pycd go && go test ./internal/e2e -run TestMCPOAuthE2E -count=1cd dotnet && DOTNET_ROLL_FORWARD=Major dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter FullyQualifiedName~McpOAuthE2ETestscd rust && export PATH="$HOME/.cargo/bin:$PATH" && cargo test --features test-support --test e2e mcp_oauth -- --nocapturecd java && mvn test -Dtest=McpOAuthE2ETestgofmt -w internal/e2e/mcp_oauth_e2e_test.go,cargo fmt -- tests/e2e/mcp_oauth.rs, andmvn spotless:apply -Dspotless.check.skip=false.