Support V1/V2 per-audience token acquisition for MCP servers#217
Merged
Support V1/V2 per-audience token acquisition for MCP servers#217
Conversation
V2 MCP servers require individual OAuth tokens scoped to their own audience GUID rather than the shared ATG token used by V1 servers. - Add audience, scope, publisher, headers fields to MCPServerConfig - Add resolve_token_scope_for_server() to determine OAuth scope: V2 servers (GUID audience) get <audience>/.default, V1 servers fall back to the shared ATG scope - Add _attach_per_audience_tokens() to McpToolServerConfigurationService: acquires one token per unique audience (cached), attaches Authorization header to each server config after discovery - Extend list_tool_servers() to accept optional authorization context; calls _attach_per_audience_tokens() when provided - Preserve audience/scope/publisher fields in manifest and gateway parsers - Update McpToolRegistrationService to pass auth context to list_tool_servers() and use per-server headers instead of a single shared token - Update tests to reflect the new header flow All V1 agents continue working unchanged (audience defaults to None, falls back to ATG scope). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dependency ReviewThe following issues were found:
License Issuesuv.lock
OpenSSF ScorecardScorecard details
Scanned Files
|
- Bump gateway discovery endpoint to /agents/v2/{id}/mcpServers
- Update resolve_token_scope_for_server() to use explicit scope when
present (e.g. Tools.ListInvoke.All → {audience}/{scope}), falling
back to {audience}/.default for pre-consented scopes when null
- Fix api:// V2 audience handling — only ATG AppId in api:// URI form
is treated as V1; all other api:// audiences are correctly V2
- Pass authorization/auth_handler_name/turn_context to list_tool_servers
in OpenAI, Semantic Kernel, and Google ADK extensions so
_attach_per_audience_tokens runs for all frameworks (was previously
only wired in AgentFramework extension)
- Replace shared single-token header injection with per-server header
merge ({**base_headers, **server.headers}) in all three extensions
- Add tests for resolve_token_scope_for_server (all V1/V2/null/api://
scenarios), _attach_per_audience_tokens (V1 dedup, V2 per-audience,
mixed, error cases, header preservation)
Backward compatible: V1 agents (null/ATG audience) continue using the
shared ATG token unchanged. Row 3 (New blueprint + Old SDK) is by
design not supported — upgrade to this SDK is the migration path.
- Pass authorization/auth_handler_name/turn_context to list_tool_servers
in OpenAI and Semantic Kernel extensions so _attach_per_audience_tokens
runs for all frameworks (Google ADK already staged in prior commit)
- Replace shared single-token header injection with per-server header
merge ({**base_headers, **server.headers}) in OpenAI and SK extensions
- Use explicit scope for V2 token resolution: {audience}/{scope} when
scope is present, {audience}/.default as pre-consented fallback
- Fix api:// V2 audience handling — only ATG AppId in api:// URI form
is treated as V1; all other api:// audiences are correctly V2
- Add comprehensive tests for resolve_token_scope_for_server covering
all V1/V2/null/api:// scenarios including test env audience handling
- Add CHANGELOG entry for microsoft-agents-a365-tooling package
Backward compatible: V1 agents (null/ATG audience) continue using the
shared ATG token unchanged. V2 blueprint + old SDK is by design not
supported — upgrade to this SDK is the migration path.
- Normalize "default" audience → None and "null" scope → None in both _parse_manifest_server_config() and _parse_gateway_server_config() so Dataverse-style servers are correctly treated as V1 (shared ATG token) instead of triggering a bogus V2 token exchange - Add "default" guard to resolve_token_scope_for_server() as defense-in-depth - Fall back to mcpServerName when mcpServerUniqueName is absent from the manifest or gateway response - Add _attach_dev_tokens() — reads BEARER_TOKEN_<SERVER_UNIQUE_NAME> and BEARER_TOKEN env vars written by `a365 develop get-token` and attaches per-server Authorization headers during local dev manifest loading; no-op in production where OBO via _attach_per_audience_tokens() is used
…context from add_tool_servers_to_agent into _get_mcp_tool_definitions_and_resources, which now passes them as keyword args to list_tool_servers() so _attach_per_audience_tokens() runs for V2 servers. Header injection reads server.headers (per-audience token) with fallback to the shared auth_token. test_mcp_tool_registration_service.py — 7 tests covering: auth context forwarding, per-audience token used over shared token, fallback when headers empty, no double Bearer prefix, User-Agent header, and per-server isolation across multiple servers.
…dless of what auth context the extension passes Prod mode (gateway): OBO runs only when auth context is present
Replace separate _attach_dev_tokens() / _attach_per_audience_tokens(auth, handler, ctx) with a unified _attach_per_audience_tokens(servers, acquire: TokenAcquirer) that accepts a pluggable async callable — aligning with Node.js PR #226. - Add TokenAcquirer type alias: Callable[[MCPServerConfig, str], Awaitable[Optional[str]]] - Add _create_dev_token_acquirer(): reads BEARER_TOKEN_<MCP_SERVER_NAME_UPPER> (now using mcp_server_name, not mcp_server_unique_name, to match Node.js) with BEARER_TOKEN fallback - Add _create_obo_token_acquirer(): performs OBO exchange per unique audience scope - Remove _attach_dev_tokens() entirely - list_tool_servers(): dev branch uses dev acquirer; prod branch uses OBO acquirer when auth context present; both paths go through the unified _attach_per_audience_tokens() - Update TestAttachPerAudienceTokens tests to use _create_obo_token_acquirer() helper
Propagates the inbound activity ID (turn_context.activity.id) as the x-ms-correlation-id header on all tooling gateway requests, falling back to a freshly generated UUID4 when no TurnContext is available.
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for per-audience OAuth token acquisition so V2 MCP servers receive audience-scoped tokens (while preserving V1 shared ATG behavior), then threads the resulting per-server headers through tool registration flows and tests.
Changes:
- Introduces
audience/scope/publisher/headersonMCPServerConfigand preserves them in manifest/gateway parsing. - Adds
resolve_token_scope_for_server()and_attach_per_audience_tokens()to acquire/deduplicate tokens per resolved scope and attachAuthorizationper server. - Updates extensions and tests to use per-server headers (and adds correlation-id support on gateway requests).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/tooling/test_mcp_server_configuration.py | Adds test coverage for audience parsing, scope resolution, and per-audience token attachment behavior |
| tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py | New unit tests validating that per-server Authorization is used and auth context is forwarded |
| tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py | Updates tests to reflect per-server headers attached during discovery |
| libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py | Adds scope resolution helper and bumps gateway endpoint to /agents/v2/... |
| libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/constants.py | Adds correlation-id header constant |
| libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py | Implements token acquisition/attachment + correlation-id header generation and parser field propagation |
| libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py | Extends config model with headers/audience/scope/publisher |
| libraries/microsoft-agents-a365-tooling/CHANGELOG.md | Documents new V1/V2 token behavior and endpoint change |
| libraries/microsoft-agents-a365-tooling-extensions-semantickernel/.../mcp_tool_registration_service.py | Merges base headers with per-server headers from discovery |
| libraries/microsoft-agents-a365-tooling-extensions-openai/.../mcp_tool_registration_service.py | Propagates server_config.headers and uses per-server headers when creating clients |
| libraries/microsoft-agents-a365-tooling-extensions-googleadk/.../mcp_tool_registration_service.py | Switches from a single shared auth header to per-server headers |
| libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/.../mcp_tool_registration_service.py | Uses per-server Authorization if present and falls back defensively to discovery token |
| libraries/microsoft-agents-a365-tooling-extensions-agentframework/.../mcp_tool_registration_service.py | Passes auth context and merges base headers with per-server headers |
- McpToolServerConfigurationService: add _create_dev_token_acquirer() and _create_obo_token_acquirer() TokenAcquirer factories; _attach_per_audience_tokens() now accepts a TokenAcquirer to decouple token strategy from header attachment - Strip existing Bearer prefix (case-insensitive) from BEARER_TOKEN* env vars in dev acquirer to prevent doubled Authorization headers - Replace manual MCPServerConfig reconstruction with dataclasses.replace() in _attach_per_audience_tokens() so future fields are carried forward automatically - Merge _parse_manifest_server_config() and _parse_gateway_server_config() into single _parse_server_config() (shared JSON schema between both sources) - utility.py: add is_development_environment() with 4-level env var resolution (PYTHON_ENVIRONMENT > ENVIRONMENT > ASPNETCORE_ENVIRONMENT > DOTNET_ENVIRONMENT); normalize audience to lowercase in resolve_token_scope_for_server() to ensure consistent V1/V2 classification regardless of GUID casing - OpenAI, Semantic Kernel, Google ADK, Agent Framework extensions: pass auth context to list_tool_servers(), skip token exchange in dev mode, add auth fallback header when no per-server token is present; fix typing.Any usages - CHANGELOG: correct stale method names (_attach_dev_tokens -> _create_dev_token_acquirer, _parse_*_server_config -> _parse_server_config) and env var naming (BEARER_TOKEN_<SERVER_UNIQUE_NAME> -> BEARER_TOKEN_<MCP_SERVER_NAME_UPPER>) - Tests: add coverage for Bearer prefix stripping (4 casing variants), raw token passthrough, per-server env var stripping; fix test fixtures for merged parse method and json() mock pattern
- Replace agent-framework-azure-ai (pre-release only) with agent-framework >= 1.0.0 in root pyproject.toml constraint-dependencies and agentframework extension package; agent-framework-core is now pinned to >= 1.0.0 as a separate explicit constraint - agent-framework 1.0.1 (GA) bundles Azure OpenAI support directly in OpenAIChatClient via credential/azure_endpoint params; AzureOpenAIChatClient no longer exists - Update mcp_tool_registration_service.py (agentframework extension): - Import HistoryProvider (renamed from BaseHistoryProvider in GA) - Replace Union[OpenAIChatClient, AzureOpenAIChatClient] with OpenAIChatClient - Remove agent_framework.azure import; remove unused Union import - Update test fixtures and docstrings to reflect renamed types - Revert google-adk constraint to >= 1.0.0 (from >= 1.28.1): google-adk >= 1.28.1 pins opentelemetry-api < 1.39.0 while agent-framework-core >= 1.0.0 requires opentelemetry-api >= 1.39.0; no compatible intersection exists. CVE-2026-4810 (GHSA-rg7c-g689-fr3x) affects ADK Web server mode only; this SDK uses google-adk purely as a library and does not expose ADK Web, so the vulnerability is not applicable.
Collapse multi-line logger.info() call to single line to satisfy ruff format --check (line fits within 100-char limit). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mcp_tool_server_configuration_service.py: replace 'mirrors Node.js behaviour' and 'aligns with Node.js' with self-contained descriptions - utility.py: rephrase is_development_environment() docstring to describe env var precedence without mentioning .NET (ASPNETCORE_ENVIRONMENT and DOTNET_ENVIRONMENT variable names are kept, only the platform labels are removed) - test_mcp_server_configuration.py: remove reference to non-existent MCP_PLATFORM_APP_ID env var in test section header and docstring; replace with accurate guidance pointing to MCP_PLATFORM_AUTHENTICATION_SCOPE / MCP_PLATFORM_ENDPOINT as the actual configurable inputs
In some CI environments (Linux Python 3.12) agent_framework loads but its top-level namespace is missing RawAgent/MCPStreamableHTTPTool due to a partial circular-import during pytest collection. The new conftest.py is loaded by pytest before test files in this directory tree are imported, and adds MagicMock stubs for any absent names so the module-level `from agent_framework import ...` in the production service succeeds.
The agentframework, semantickernel, and openai extensions unconditionally prepended BEARER_PREFIX to auth_token in the fallback Authorization header, producing "Bearer Bearer <token>" when callers already included the prefix. Align with the azureaifoundry extension by applying the same case-insensitive startswith guard before prepending the prefix.
agent_framework.openai causes a circular-import chain under Python 3.12 on Linux: openai.__getattr__ lazily imports agent_framework_openai, which eventually does `from . import __version__` on the still-initialising agent_framework package. Move OpenAIChatClient under TYPE_CHECKING so the import is never executed at runtime. Also add the missing is_development_environment() guard to the Azure AI Foundry extension's add_tool_servers_to_agent, matching the pattern used by the other three extensions: dev mode skips token exchange and uses "" as the agentic_app_id for manifest-based discovery.
…th tests _create_dev_token_acquirer now emits a WARNING when BEARER_TOKEN is the only token available but the server requires a different audience scope (V2 server). The warning names the per-server env var the caller should set to avoid a 401. Also adds two unit tests that verify the existing legacy production-path guard: a hard error is raised when list_tool_servers is called without auth context and V2 servers are discovered, and the call succeeds when only V1 servers are present.
googleadk unconditionally prepended 'Bearer' in the shared-token fallback path, producing 'Bearer Bearer <token>' when the caller already included the prefix. Apply the same guard used in the other three extensions.
ajmfehr
approved these changes
Apr 16, 2026
gwharris7
approved these changes
Apr 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
V2 MCP servers require individual OAuth tokens scoped to their own audience GUID rather than the shared ATG token used by V1 servers.
All V1 agents continue working unchanged (audience defaults to None, falls back to ATG scope).