Skip to content

Commit dc936b1

Browse files
biswapmclaude
andauthored
Support V1/V2 per-audience token acquisition for MCP servers (#217)
* Support V1/V2 per-audience token acquisition for MCP servers 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> * Extend V1/V2 per-audience token support to all SDK extensions - 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. * Extend V1/V2 per-audience token support to all SDK extensions - 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. * Fix manifest/gateway parsing and add local dev per-server token support - 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 * fixed formatting * mcp_tool_registration_service.py — threaded auth, auth_handler_name, 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. * Dev mode (manifest): _attach_dev_tokens() runs, OBO never fires regardless of what auth context the extension passes Prod mode (gateway): OBO runs only when auth context is present * Refactor token acquisition to TokenAcquirer strategy pattern 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 * updated prod endpoint * Add x-ms-correlation-id header to gateway requests 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. * Add MCP V1/V2 per-audience token acquisition and dev token flow - 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 * Migrate agent-framework to GA 1.0.1; revert google-adk to 1.14.1 - 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. * Fix ruff formatting in semantickernel mcp_tool_registration_service 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> * Remove cross-platform references from comments and docstrings - 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 * Add conftest.py to guard agent_framework imports in CI unit tests 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. * Strip Bearer prefix in fallback auth header across all three extensions 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. * Fix CI import failures and missing is_dev guard 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. * Add dev-mode warning for V2 servers using shared token, add legacy-path 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. * Fix Bearer double-prefix in googleadk auth fallback 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. --------- Co-authored-by: pmohapatra <biswapm@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f93147c commit dc936b1

19 files changed

Lines changed: 2632 additions & 306 deletions

File tree

libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
from __future__ import annotations
5+
46
import logging
57
import uuid
68
from datetime import datetime, timezone
7-
from typing import Any, List, Optional, Sequence, Union
9+
from typing import TYPE_CHECKING, List, Optional, Sequence
810

9-
from agent_framework import RawAgent, Message, BaseHistoryProvider, MCPStreamableHTTPTool
10-
from agent_framework.azure import AzureOpenAIChatClient
11-
from agent_framework.openai import OpenAIChatClient
11+
from agent_framework import RawAgent, Message, HistoryProvider, MCPStreamableHTTPTool
1212
import httpx
1313

14+
if TYPE_CHECKING:
15+
from agent_framework.openai import OpenAIChatClient
16+
1417
from microsoft_agents.hosting.core import Authorization, TurnContext
1518

1619
from microsoft_agents_a365.runtime import OperationResult
@@ -22,6 +25,7 @@
2225
from microsoft_agents_a365.tooling.utils.constants import Constants
2326
from microsoft_agents_a365.tooling.utils.utility import (
2427
get_mcp_platform_authentication_scope,
28+
is_development_environment,
2529
)
2630

2731

@@ -55,9 +59,9 @@ def __init__(self, logger: Optional[logging.Logger] = None):
5559

5660
async def add_tool_servers_to_agent(
5761
self,
58-
chat_client: Union[OpenAIChatClient, AzureOpenAIChatClient],
62+
chat_client: OpenAIChatClient,
5963
agent_instructions: str,
60-
initial_tools: List[Any],
64+
initial_tools: List[object],
6165
auth: Authorization,
6266
auth_handler_name: str,
6367
turn_context: TurnContext,
@@ -67,7 +71,7 @@ async def add_tool_servers_to_agent(
6771
Add MCP tool servers to a RawAgent (mirrors .NET implementation).
6872
6973
Args:
70-
chat_client: The chat client instance (Union[OpenAIChatClient, AzureOpenAIChatClient])
74+
chat_client: The chat client instance (OpenAIChatClient supports both OpenAI and Azure OpenAI)
7175
agent_instructions: Instructions for the agent behavior
7276
initial_tools: List of initial tools to add to the agent
7377
auth: Authorization context for token exchange
@@ -82,23 +86,31 @@ async def add_tool_servers_to_agent(
8286
Exception: If agent creation fails.
8387
"""
8488
try:
85-
# Exchange token if not provided
86-
if not auth_token:
89+
is_dev = is_development_environment()
90+
if not auth_token and not is_dev:
91+
# Only exchange a token in production; dev mode uses BEARER_TOKEN* env vars instead.
8792
scopes = get_mcp_platform_authentication_scope()
8893
authToken = await auth.exchange_token(turn_context, scopes, auth_handler_name)
8994
auth_token = authToken.token
9095

91-
agentic_app_id = Utility.resolve_agent_identity(turn_context, auth_token)
96+
# In dev mode, agentic_app_id is not needed for manifest-based discovery.
97+
agentic_app_id = (
98+
"" if is_dev else Utility.resolve_agent_identity(turn_context, auth_token)
99+
)
92100

93101
self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
94102

95103
options = ToolOptions(orchestrator_name=self._orchestrator_name)
96104

97-
# Get MCP server configurations
105+
# Get MCP server configurations — pass auth context so each server receives
106+
# its own per-audience Authorization token (V1 = shared ATG, V2 = per-GUID).
98107
server_configs = await self._mcp_server_configuration_service.list_tool_servers(
99108
agentic_app_id=agentic_app_id,
100109
auth_token=auth_token,
101110
options=options,
111+
authorization=auth,
112+
auth_handler_name=auth_handler_name,
113+
turn_context=turn_context,
102114
)
103115

104116
self._logger.info(f"Loaded {len(server_configs)} MCP server configurations")
@@ -112,16 +124,26 @@ async def add_tool_servers_to_agent(
112124
server_name = config.mcp_server_name or config.mcp_server_unique_name
113125

114126
try:
115-
# Prepare auth headers
116-
headers = {}
117-
if auth_token:
118-
headers[Constants.Headers.AUTHORIZATION] = (
119-
f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
127+
# Merge base (non-auth) headers with per-server headers from list_tool_servers.
128+
# server.headers already contains the correct per-audience Authorization token.
129+
base_headers = {
130+
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(
131+
self._orchestrator_name
120132
)
121-
122-
headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
123-
self._orchestrator_name
124-
)
133+
}
134+
server_headers = dict(config.headers) if config.headers else {}
135+
# Fall back to the shared discovery token when no per-server
136+
# Authorization header was attached (e.g. dev mode without
137+
# BEARER_TOKEN* env vars, or legacy V1 callers).
138+
if Constants.Headers.AUTHORIZATION not in server_headers and auth_token:
139+
server_headers[Constants.Headers.AUTHORIZATION] = (
140+
auth_token
141+
if auth_token.lower().startswith(
142+
f"{Constants.Headers.BEARER_PREFIX.lower()} "
143+
)
144+
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
145+
)
146+
headers = {**base_headers, **server_headers} # server auth takes precedence
125147

126148
# Create httpx client with auth headers configured
127149
http_client = httpx.AsyncClient(
@@ -307,18 +329,18 @@ async def send_chat_history_messages(
307329

308330
async def send_chat_history_from_store(
309331
self,
310-
chat_message_store: BaseHistoryProvider,
332+
chat_message_store: HistoryProvider,
311333
turn_context: TurnContext,
312334
tool_options: Optional[ToolOptions] = None,
313335
) -> OperationResult:
314336
"""
315-
Send chat history from a BaseHistoryProvider to the MCP platform.
337+
Send chat history from a HistoryProvider to the MCP platform.
316338
317339
This is a convenience method that extracts messages from the store
318340
and delegates to send_chat_history_messages().
319341
320342
Args:
321-
chat_message_store: BaseHistoryProvider containing the conversation history.
343+
chat_message_store: HistoryProvider containing the conversation history.
322344
turn_context: TurnContext from the Agents SDK containing conversation info.
323345
tool_options: Optional configuration for the request.
324346

libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ license = {text = "MIT"}
2626
dependencies = [
2727
"microsoft-agents-a365-tooling",
2828
"microsoft-agents-hosting-core",
29-
"agent-framework-azure-ai",
29+
"agent-framework",
3030
"azure-identity",
3131
"typing-extensions",
3232
"httpx",

libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
McpToolServerConfigurationService,
2828
)
2929
from microsoft_agents_a365.tooling.utils.constants import Constants
30-
from microsoft_agents_a365.tooling.utils.utility import get_mcp_platform_authentication_scope
30+
from microsoft_agents_a365.tooling.utils.utility import (
31+
get_mcp_platform_authentication_scope,
32+
is_development_environment,
33+
)
3134

3235

3336
class McpToolRegistrationService:
@@ -99,16 +102,18 @@ async def add_tool_servers_to_agent(
99102
if project_client is None:
100103
raise ValueError("project_client cannot be None")
101104

102-
if not auth_token:
105+
is_dev = is_development_environment()
106+
if not auth_token and not is_dev:
103107
scopes = get_mcp_platform_authentication_scope()
104108
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
105109
auth_token = authToken.token
106110

107111
try:
108-
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
109-
# Get the tool definitions and resources using the async implementation
112+
agentic_app_id = "" if is_dev else Utility.resolve_agent_identity(context, auth_token)
113+
# Get the tool definitions and resources — pass auth context so each server receives
114+
# its own per-audience Authorization token (V1 = shared ATG, V2 = per-GUID).
110115
tool_definitions, tool_resources = await self._get_mcp_tool_definitions_and_resources(
111-
agentic_app_id, auth_token or ""
116+
agentic_app_id, auth_token or "", auth, auth_handler_name, context
112117
)
113118

114119
# Update the agent with the tools
@@ -127,7 +132,12 @@ async def add_tool_servers_to_agent(
127132
raise
128133

129134
async def _get_mcp_tool_definitions_and_resources(
130-
self, agentic_app_id: str, auth_token: str
135+
self,
136+
agentic_app_id: str,
137+
auth_token: str,
138+
authorization: Authorization,
139+
auth_handler_name: str,
140+
turn_context: TurnContext,
131141
) -> Tuple[List[McpTool], Optional[ToolResources]]:
132142
"""
133143
Internal method to get MCP tool definitions and resources.
@@ -136,7 +146,10 @@ async def _get_mcp_tool_definitions_and_resources(
136146
137147
Args:
138148
agentic_app_id: Agentic App ID for the agent.
139-
auth_token: Authentication token to access the MCP servers.
149+
auth_token: Authentication token used for gateway discovery.
150+
authorization: Authorization context for per-audience token exchange.
151+
auth_handler_name: Auth handler name for per-audience token exchange.
152+
turn_context: TurnContext for per-audience token exchange.
140153
141154
Returns:
142155
Tuple containing tool definitions and resources.
@@ -145,11 +158,17 @@ async def _get_mcp_tool_definitions_and_resources(
145158
self._logger.error("MCP server configuration service is not available")
146159
return ([], None)
147160

148-
# Get MCP server configurations
161+
# Get MCP server configurations — pass auth context so each server receives
162+
# its own per-audience Authorization token (V1 = shared ATG, V2 = per-GUID).
149163
options = ToolOptions(orchestrator_name=self._orchestrator_name)
150164
try:
151165
servers = await self._mcp_server_configuration_service.list_tool_servers(
152-
agentic_app_id, auth_token, options
166+
agentic_app_id,
167+
auth_token,
168+
options,
169+
authorization=authorization,
170+
auth_handler_name=auth_handler_name,
171+
turn_context=turn_context,
153172
)
154173
except Exception as ex:
155174
self._logger.error(
@@ -191,14 +210,19 @@ async def _get_mcp_tool_definitions_and_resources(
191210
# Configure the tool
192211
mcp_tool.set_approval_mode("never")
193212

194-
# Set up authorization header
195-
if auth_token:
196-
header_value = (
213+
# Set per-server headers: use per-audience token from list_tool_servers
214+
# (V1 servers get the shared ATG token, V2 servers get their own audience token).
215+
# Fall back to the shared discovery auth_token only if no per-server header is set.
216+
server_headers = dict(server.headers) if server.headers else {}
217+
auth_header = server_headers.get(Constants.Headers.AUTHORIZATION)
218+
if not auth_header and auth_token:
219+
auth_header = (
197220
auth_token
198221
if auth_token.lower().startswith(f"{Constants.Headers.BEARER_PREFIX.lower()} ")
199222
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
200223
)
201-
mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, header_value)
224+
if auth_header:
225+
mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, auth_header)
202226

203227
mcp_tool.update_headers(
204228
Constants.Headers.USER_AGENT, Utility.get_user_agent_header(self._orchestrator_name)

libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from microsoft_agents_a365.tooling.utils.constants import Constants
2727
from microsoft_agents_a365.tooling.utils.utility import (
2828
get_mcp_platform_authentication_scope,
29+
is_development_environment,
2930
)
3031

3132

@@ -76,19 +77,25 @@ async def add_tool_servers_to_agent(
7677
Returns:
7778
None
7879
"""
79-
if not auth_token:
80+
is_dev = is_development_environment()
81+
if not auth_token and not is_dev:
82+
# Only exchange a token in production; dev mode uses BEARER_TOKEN* env vars instead.
8083
scopes = get_mcp_platform_authentication_scope()
8184
auth_token_obj = await auth.exchange_token(context, scopes, auth_handler_name)
8285
auth_token = auth_token_obj.token
8386

84-
agentic_app_id = Utility.resolve_agent_identity(context, auth_token)
87+
# In dev mode, agentic_app_id is not needed for manifest-based discovery.
88+
agentic_app_id = "" if is_dev else Utility.resolve_agent_identity(context, auth_token)
8589
self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}")
8690

8791
options = ToolOptions(orchestrator_name=self._orchestrator_name)
8892
mcp_server_configs = await self._mcp_server_configuration_service.list_tool_servers(
8993
agentic_app_id=agentic_app_id,
9094
auth_token=auth_token,
9195
options=options,
96+
authorization=auth,
97+
auth_handler_name=auth_handler_name,
98+
turn_context=context,
9299
)
93100

94101
self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations")
@@ -104,10 +111,6 @@ async def add_tool_servers_to_agent(
104111

105112
# Convert MCP server configs to McpToolset objects (only new ones)
106113
mcp_servers_info = []
107-
mcp_server_headers = {
108-
Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
109-
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(self._orchestrator_name),
110-
}
111114

112115
for server_config in mcp_server_configs:
113116
# Skip if server URL already exists
@@ -119,10 +122,29 @@ async def add_tool_servers_to_agent(
119122
continue
120123

121124
try:
125+
base_headers = {
126+
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(
127+
self._orchestrator_name
128+
)
129+
}
130+
server_headers = dict(server_config.headers) if server_config.headers else {}
131+
# Fall back to the shared discovery token when no per-server
132+
# Authorization header was attached (e.g. dev mode without
133+
# BEARER_TOKEN* env vars, or legacy V1 callers).
134+
if Constants.Headers.AUTHORIZATION not in server_headers and auth_token:
135+
server_headers[Constants.Headers.AUTHORIZATION] = (
136+
auth_token
137+
if auth_token.lower().startswith(
138+
f"{Constants.Headers.BEARER_PREFIX.lower()} "
139+
)
140+
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
141+
)
142+
headers = {**base_headers, **server_headers} # per-audience token takes precedence
143+
122144
server_info = McpToolset(
123145
connection_params=StreamableHTTPConnectionParams(
124146
url=server_config.url,
125-
headers=mcp_server_headers,
147+
headers=headers,
126148
)
127149
)
128150

0 commit comments

Comments
 (0)