Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c52f367
Support V1/V2 per-audience token acquisition for MCP servers
biswapm Mar 31, 2026
d84c41d
Extend V1/V2 per-audience token support to all SDK extensions
biswapm Apr 3, 2026
74451d7
Extend V1/V2 per-audience token support to all SDK extensions
biswapm Apr 3, 2026
f3b76d4
Fix manifest/gateway parsing and add local dev per-server token support
biswapm Apr 9, 2026
b1f6a3b
fixed formatting
biswapm Apr 9, 2026
4cb88c9
mcp_tool_registration_service.py — threaded auth, auth_handler_name, …
biswapm Apr 12, 2026
a135403
Dev mode (manifest): _attach_dev_tokens() runs, OBO never fires regar…
biswapm Apr 12, 2026
31a6685
Refactor token acquisition to TokenAcquirer strategy pattern
biswapm Apr 12, 2026
8cc5e32
updated prod endpoint
biswapm Apr 14, 2026
8b08ac2
Merge remote-tracking branch 'origin/main' into pmohapatra-MCP-V1-V2-…
biswapm Apr 15, 2026
77fc5d8
Add x-ms-correlation-id header to gateway requests
biswapm Apr 15, 2026
cac2892
Add MCP V1/V2 per-audience token acquisition and dev token flow
biswapm Apr 16, 2026
6f340d6
Migrate agent-framework to GA 1.0.1; revert google-adk to 1.14.1
biswapm Apr 16, 2026
9ef600e
Fix ruff formatting in semantickernel mcp_tool_registration_service
biswapm Apr 16, 2026
3dd83fe
Remove cross-platform references from comments and docstrings
biswapm Apr 16, 2026
48e4683
Add conftest.py to guard agent_framework imports in CI unit tests
biswapm Apr 16, 2026
f2f5f51
Strip Bearer prefix in fallback auth header across all three extensions
biswapm Apr 16, 2026
3fde2a2
Fix CI import failures and missing is_dev guard
biswapm Apr 16, 2026
5e91a28
Add dev-mode warning for V2 servers using shared token, add legacy-pa…
biswapm Apr 16, 2026
6b64061
Fix Bearer double-prefix in googleadk auth fallback
biswapm Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from __future__ import annotations

import logging
import uuid
from datetime import datetime, timezone
from typing import Any, List, Optional, Sequence, Union
from typing import TYPE_CHECKING, List, Optional, Sequence

from agent_framework import RawAgent, Message, BaseHistoryProvider, MCPStreamableHTTPTool
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.openai import OpenAIChatClient
from agent_framework import RawAgent, Message, HistoryProvider, MCPStreamableHTTPTool
import httpx

if TYPE_CHECKING:
from agent_framework.openai import OpenAIChatClient

from microsoft_agents.hosting.core import Authorization, TurnContext

from microsoft_agents_a365.runtime import OperationResult
Expand All @@ -22,6 +25,7 @@
from microsoft_agents_a365.tooling.utils.constants import Constants
from microsoft_agents_a365.tooling.utils.utility import (
get_mcp_platform_authentication_scope,
is_development_environment,
)


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

async def add_tool_servers_to_agent(
self,
chat_client: Union[OpenAIChatClient, AzureOpenAIChatClient],
chat_client: OpenAIChatClient,
agent_instructions: str,
initial_tools: List[Any],
initial_tools: List[object],
auth: Authorization,
auth_handler_name: str,
turn_context: TurnContext,
Expand All @@ -67,7 +71,7 @@ async def add_tool_servers_to_agent(
Add MCP tool servers to a RawAgent (mirrors .NET implementation).

Args:
chat_client: The chat client instance (Union[OpenAIChatClient, AzureOpenAIChatClient])
chat_client: The chat client instance (OpenAIChatClient supports both OpenAI and Azure OpenAI)
agent_instructions: Instructions for the agent behavior
initial_tools: List of initial tools to add to the agent
auth: Authorization context for token exchange
Expand All @@ -82,23 +86,31 @@ async def add_tool_servers_to_agent(
Exception: If agent creation fails.
"""
try:
# Exchange token if not provided
if not auth_token:
is_dev = is_development_environment()
if not auth_token and not is_dev:
# Only exchange a token in production; dev mode uses BEARER_TOKEN* env vars instead.
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(turn_context, scopes, auth_handler_name)
auth_token = authToken.token

agentic_app_id = Utility.resolve_agent_identity(turn_context, auth_token)
# In dev mode, agentic_app_id is not needed for manifest-based discovery.
agentic_app_id = (
"" if is_dev else Utility.resolve_agent_identity(turn_context, auth_token)
)

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

options = ToolOptions(orchestrator_name=self._orchestrator_name)

# Get MCP server configurations
# Get MCP server configurations — pass auth context so each server receives
# its own per-audience Authorization token (V1 = shared ATG, V2 = per-GUID).
server_configs = await self._mcp_server_configuration_service.list_tool_servers(
agentic_app_id=agentic_app_id,
auth_token=auth_token,
options=options,
authorization=auth,
auth_handler_name=auth_handler_name,
turn_context=turn_context,
)

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

try:
# Prepare auth headers
headers = {}
if auth_token:
headers[Constants.Headers.AUTHORIZATION] = (
f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
# Merge base (non-auth) headers with per-server headers from list_tool_servers.
# server.headers already contains the correct per-audience Authorization token.
base_headers = {
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(
self._orchestrator_name
)

headers[Constants.Headers.USER_AGENT] = Utility.get_user_agent_header(
self._orchestrator_name
)
}
server_headers = dict(config.headers) if config.headers else {}
# Fall back to the shared discovery token when no per-server
# Authorization header was attached (e.g. dev mode without
# BEARER_TOKEN* env vars, or legacy V1 callers).
if Constants.Headers.AUTHORIZATION not in server_headers and auth_token:
server_headers[Constants.Headers.AUTHORIZATION] = (
auth_token
if auth_token.lower().startswith(
f"{Constants.Headers.BEARER_PREFIX.lower()} "
)
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
)
headers = {**base_headers, **server_headers} # server auth takes precedence

# Create httpx client with auth headers configured
http_client = httpx.AsyncClient(
Expand Down Expand Up @@ -307,18 +329,18 @@ async def send_chat_history_messages(

async def send_chat_history_from_store(
self,
chat_message_store: BaseHistoryProvider,
chat_message_store: HistoryProvider,
turn_context: TurnContext,
tool_options: Optional[ToolOptions] = None,
) -> OperationResult:
"""
Send chat history from a BaseHistoryProvider to the MCP platform.
Send chat history from a HistoryProvider to the MCP platform.

This is a convenience method that extracts messages from the store
and delegates to send_chat_history_messages().

Args:
chat_message_store: BaseHistoryProvider containing the conversation history.
chat_message_store: HistoryProvider containing the conversation history.
turn_context: TurnContext from the Agents SDK containing conversation info.
tool_options: Optional configuration for the request.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ license = {text = "MIT"}
dependencies = [
"microsoft-agents-a365-tooling",
"microsoft-agents-hosting-core",
"agent-framework-azure-ai",
"agent-framework",
"azure-identity",
"typing-extensions",
"httpx",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
McpToolServerConfigurationService,
)
from microsoft_agents_a365.tooling.utils.constants import Constants
from microsoft_agents_a365.tooling.utils.utility import get_mcp_platform_authentication_scope
from microsoft_agents_a365.tooling.utils.utility import (
get_mcp_platform_authentication_scope,
is_development_environment,
)


class McpToolRegistrationService:
Expand Down Expand Up @@ -99,16 +102,18 @@ async def add_tool_servers_to_agent(
if project_client is None:
raise ValueError("project_client cannot be None")

if not auth_token:
is_dev = is_development_environment()
if not auth_token and not is_dev:
scopes = get_mcp_platform_authentication_scope()
authToken = await auth.exchange_token(context, scopes, auth_handler_name)
auth_token = authToken.token

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

# Update the agent with the tools
Expand All @@ -127,7 +132,12 @@ async def add_tool_servers_to_agent(
raise

async def _get_mcp_tool_definitions_and_resources(
self, agentic_app_id: str, auth_token: str
self,
agentic_app_id: str,
auth_token: str,
authorization: Authorization,
auth_handler_name: str,
turn_context: TurnContext,
) -> Tuple[List[McpTool], Optional[ToolResources]]:
"""
Internal method to get MCP tool definitions and resources.
Expand All @@ -136,7 +146,10 @@ async def _get_mcp_tool_definitions_and_resources(

Args:
agentic_app_id: Agentic App ID for the agent.
auth_token: Authentication token to access the MCP servers.
auth_token: Authentication token used for gateway discovery.
authorization: Authorization context for per-audience token exchange.
auth_handler_name: Auth handler name for per-audience token exchange.
turn_context: TurnContext for per-audience token exchange.

Returns:
Tuple containing tool definitions and resources.
Expand All @@ -145,11 +158,17 @@ async def _get_mcp_tool_definitions_and_resources(
self._logger.error("MCP server configuration service is not available")
return ([], None)

# Get MCP server configurations
# Get MCP server configurations — pass auth context so each server receives
# its own per-audience Authorization token (V1 = shared ATG, V2 = per-GUID).
options = ToolOptions(orchestrator_name=self._orchestrator_name)
try:
servers = await self._mcp_server_configuration_service.list_tool_servers(
agentic_app_id, auth_token, options
agentic_app_id,
auth_token,
options,
authorization=authorization,
auth_handler_name=auth_handler_name,
turn_context=turn_context,
)
except Exception as ex:
self._logger.error(
Expand Down Expand Up @@ -191,14 +210,19 @@ async def _get_mcp_tool_definitions_and_resources(
# Configure the tool
mcp_tool.set_approval_mode("never")

# Set up authorization header
if auth_token:
header_value = (
# Set per-server headers: use per-audience token from list_tool_servers
# (V1 servers get the shared ATG token, V2 servers get their own audience token).
# Fall back to the shared discovery auth_token only if no per-server header is set.
server_headers = dict(server.headers) if server.headers else {}
auth_header = server_headers.get(Constants.Headers.AUTHORIZATION)
if not auth_header and auth_token:
auth_header = (
auth_token
if auth_token.lower().startswith(f"{Constants.Headers.BEARER_PREFIX.lower()} ")
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
)
mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, header_value)
if auth_header:
mcp_tool.update_headers(Constants.Headers.AUTHORIZATION, auth_header)

mcp_tool.update_headers(
Constants.Headers.USER_AGENT, Utility.get_user_agent_header(self._orchestrator_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from microsoft_agents_a365.tooling.utils.constants import Constants
from microsoft_agents_a365.tooling.utils.utility import (
get_mcp_platform_authentication_scope,
is_development_environment,
)


Expand Down Expand Up @@ -76,19 +77,25 @@ async def add_tool_servers_to_agent(
Returns:
None
"""
if not auth_token:
is_dev = is_development_environment()
if not auth_token and not is_dev:
# Only exchange a token in production; dev mode uses BEARER_TOKEN* env vars instead.
scopes = get_mcp_platform_authentication_scope()
auth_token_obj = await auth.exchange_token(context, scopes, auth_handler_name)
auth_token = auth_token_obj.token

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

options = ToolOptions(orchestrator_name=self._orchestrator_name)
mcp_server_configs = await self._mcp_server_configuration_service.list_tool_servers(
agentic_app_id=agentic_app_id,
auth_token=auth_token,
options=options,
authorization=auth,
auth_handler_name=auth_handler_name,
turn_context=context,
)

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

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

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

try:
base_headers = {
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(
self._orchestrator_name
)
}
server_headers = dict(server_config.headers) if server_config.headers else {}
# Fall back to the shared discovery token when no per-server
# Authorization header was attached (e.g. dev mode without
# BEARER_TOKEN* env vars, or legacy V1 callers).
if Constants.Headers.AUTHORIZATION not in server_headers and auth_token:
server_headers[Constants.Headers.AUTHORIZATION] = (
auth_token
if auth_token.lower().startswith(
f"{Constants.Headers.BEARER_PREFIX.lower()} "
)
else f"{Constants.Headers.BEARER_PREFIX} {auth_token}"
)
headers = {**base_headers, **server_headers} # per-audience token takes precedence

server_info = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url=server_config.url,
headers=mcp_server_headers,
headers=headers,
)
)

Expand Down
Loading
Loading