Skip to content

auth: target MCP tokens by session and customization - #535

Merged
Connor Peet (connor4312) merged 2 commits into
mainfrom
connor4312/mcp-authenticate-target
Oct 20, 2026
Merged

Connor Peet (connor4312) merged 2 commits into
mainfrom
connor4312/mcp-authenticate-target

Conversation

@connor4312

Copy link
Copy Markdown
Member

Supersedes #439.

Summary

Adds an optional, extensible target to AuthenticateParams so a client can say which consumer a pushed token is for:

export const enum AuthenticateTargetKind { McpServer = 'mcpServer' } // @nonexhaustive
export interface McpServerAuthenticateTarget {
  kind: AuthenticateTargetKind.McpServer;
  session: URI;            // session whose customizations contain the MCP server
  customizationId: string; // McpServerCustomization.id
}
export type AuthenticateTarget = McpServerAuthenticateTarget;

When target is present, the host MUST apply the token only to that MCP server in that session, never to sibling servers, other sessions, or agent-level resources, even when they share resource and scopes. If the session or customization is unknown, or the customization is not an MCP server, the host rejects with InvalidParams. An empty token revokes only for that target. Without target, resource/scope matching is unchanged, and older hosts ignore the field, so no capability or version bump is needed.

Why

Two MCP servers can share one protected resource while using different OAuth clients or accounts (microsoft/vscode#342147: two Slack workspaces behind https://mcp.slack.com). Today authenticate carries only resource, scopes, and token, so the host applies the first token to every pending challenge for that resource, and one workspace's server ends up with the other's credential.

#439 proposed a bare serverName. That isn't unique across sessions or manifests, and authenticate is a root-channel command. The session URI plus customization ID identifies the server exactly, and the client only echoes host-published values: McpServerAuthRequiredState lives on the customization, and ToolCallAuthRequiredState.contributor.customizationId names it.

kind is @nonexhaustive, so host-owned authentication (#404) can add variants rather than new fields.

Validation

  • npm run generate: passed.
  • npm test: 567/568 passed. The one failure is scripts/no-redaction-marker.test.ts, which only flags files under the git-ignored local docs/.vitepress/cache, no tracked files.
  • Rust cargo test -p ahp-types: passed.
  • Go go build ./... && go test ./ahptypes/...: passed.
  • Swift swift build: passed.
  • Kotlin ./gradlew test in eclipse-temurin:17-jdk via podman: passed.
  • .NET: not run (toolchain not installed locally).

Paul (pwang347) and others added 2 commits October 20, 2026 11:04
Add an optional MCP server identity to authenticate requests and regenerate every client/schema mirror so hosts can route same-resource credentials independently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace `AuthenticateParams.serverName` with an optional, extensible `target`
discriminated union. The `mcpServer` variant identifies the consumer by session
URI and `McpServerCustomization.id`, which clients echo from host-published
state. A bare server name is not unique across sessions or manifests, and
`authenticate` is a root-channel command.

With a target, hosts MUST apply the token only to that MCP server in that
session. Without one, resource/scope matching is unchanged. `kind` is
@nonexhaustive so host-owned authentication (#404) can add variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@connor4312
Connor Peet (connor4312) merged commit 1a6b289 into main Oct 20, 2026
16 checks passed
@connor4312
Connor Peet (connor4312) deleted the connor4312/mcp-authenticate-target branch October 20, 2026 19:07
Connor Peet (connor4312) added a commit to microsoft/vscode that referenced this pull request Oct 20, 2026
* agentHost: send each MCP OAuth token only to its target server

MCP servers that share an OAuth protected resource and scopes received the same token. The host
gave the first token to all pending authentication requests for that resource, so a server could
use the credential of a different account. This change uses the new optional AHP
`authenticate.target` (microsoft/agent-host-protocol#535) to identify the server for each token.

- Backport `AuthenticateTarget` and `AuthenticateParams.target` into the generated protocol copy.
- Workbench: send `target` (host session URI and MCP customization ID) for each MCP token. Keep
  provider-level tokens untargeted. Send untargeted tokens when an older host gives no target.
- Agent host: reject a target that does not identify an MCP server in an existing session. Keep
  targeted tokens in their own slots. Do not give them to agent-level resources or `getAuthToken`.
  Keep the target when tokens are replayed.
- Copilot: give a targeted token only to the pending request of the target server in the owning
  session.
- Codex: keep targeted tokens per session and server, and reconnect only that session.
- Mission Control relay: do not use a targeted MCP token as the relay identity.
- Copilot: show a server as ready after OAuth when its new connection loads tools. The runtime
  does not send a `connected` status change after an OAuth reconnect, so the server stayed in
  "starting".

Fixes #342147

(Commit message generated by Copilot)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: keep MCP OAuth sign-ins separate for each static client

The workbench used one dynamic authentication provider for each authorization server and
resource, and ignored the OAuth client ID. MCP servers with different static clients for the same
resource used one provider. A sign-in with a different client removed the provider and its stored
tokens, so a sign-in to one server signed out the other. After a reload, silent authentication
did not find a token for that server.

- Agent host MCP authentication: give each static OAuth client its own provider ID
  (`<legacy ID> client:<client ID>`). Use the legacy provider when its stored registration already
  has the same client, so existing sign-ins continue to work.
- Do not remove a provider registration when a request uses a different client. Replace a
  registration only for the same client, for example when its secret changes.
- Add an optional provider ID to `createDynamicAuthenticationProvider`, the provider host
  delegate, and `$registerDynamicAuthProvider`. The extension host provider then keeps its
  registration and tokens under that ID. Providers without a static client keep their IDs.

Fixes #342147
Fixes #293533

(Commit message generated by Copilot)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: keep targeted MCP credentials separate for reconnect replay

The protocol client keeps each accepted `authenticate` request to replay it after a reconnect. The
replay cache used only the resource and scopes as its key. Targeted MCP tokens for servers that
share these values replaced each other and the provider credential, and a revocation for one
server removed the cached credential of another server.

- Add the authentication target (kind, session, and customization ID) to the cache key. Keep the
  key for untargeted credentials unchanged.
- Add a test that keeps the provider credential and a targeted credential when a different
  target is revoked.

Fixes #342147

(Commit message generated by Copilot)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants