From c52f3674dcc07cdc10f72139f16a10104bb847fa Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Tue, 31 Mar 2026 16:07:10 +0530 Subject: [PATCH 01/19] 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 /.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 --- .../services/mcp_tool_registration_service.py | 23 ++-- .../tooling/models/mcp_server_config.py | 14 ++- .../mcp_tool_server_configuration_service.py | 113 ++++++++++++++++-- .../tooling/utils/utility.py | 32 +++++ .../test_mcp_tool_registration_service.py | 17 ++- 5 files changed, 179 insertions(+), 20 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py index c601984c..c02bcb16 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py @@ -94,11 +94,15 @@ async def add_tool_servers_to_agent( 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") @@ -112,16 +116,15 @@ 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 {} + headers = {**base_headers, **server_headers} # server auth takes precedence # Create httpx client with auth headers configured http_client = httpx.AsyncClient( diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py index 42ab90bb..dd57ff98 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py @@ -6,7 +6,7 @@ """ from dataclasses import dataclass -from typing import Optional +from typing import Dict, Optional @dataclass @@ -25,6 +25,18 @@ class MCPServerConfig: #: instead of constructing the URL from the base URL and unique name. url: Optional[str] = None + #: Per-server HTTP headers (includes the Authorization header set by attach_per_audience_tokens). + headers: Optional[Dict[str, str]] = None + + #: Per-server AppId (V2) or shared ATG AppId (V1). None means treat as V1. + audience: Optional[str] = None + + #: OAuth scope, e.g. "Tools.ListInvoke.All" (V2) or "McpServers.Mail.All" (V1). + scope: Optional[str] = None + + #: Publisher identifier for the MCP server. + publisher: Optional[str] = None + def __post_init__(self): """Validate the configuration after initialization.""" if not self.mcp_server_name: diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index c56ab4bb..bd47940c 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -29,15 +29,16 @@ # Third-party imports import aiohttp -from microsoft_agents.hosting.core import TurnContext +from microsoft_agents.hosting.core import Authorization, TurnContext # Local imports from ..models import ChatHistoryMessage, ChatMessageRequest, MCPServerConfig, ToolOptions from ..utils import Constants from ..utils.utility import ( - get_tooling_gateway_for_digital_worker, build_mcp_server_url, get_chat_history_endpoint, + get_tooling_gateway_for_digital_worker, + resolve_token_scope_for_server, ) # Runtime Imports @@ -89,22 +90,38 @@ def __init__(self, logger: Optional[logging.Logger] = None): # -------------------------------------------------------------------------- async def list_tool_servers( - self, agentic_app_id: str, auth_token: str, options: Optional[ToolOptions] = None + self, + agentic_app_id: str, + auth_token: str, + options: Optional[ToolOptions] = None, + authorization: Optional[Authorization] = None, + auth_handler_name: Optional[str] = None, + turn_context: Optional[TurnContext] = None, ) -> List[MCPServerConfig]: """ Gets the list of MCP Servers that are configured for the agent. + When ``authorization``, ``auth_handler_name``, and ``turn_context`` are all provided, + per-audience OAuth tokens are acquired for each server after discovery: + - V1 servers (no ``audience`` field) share the shared ATG token (one exchange). + - V2 servers each receive a token scoped to their own audience GUID. + 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. options: Optional ToolOptions instance containing optional parameters. + authorization: Optional Authorization context for per-audience token exchange. + auth_handler_name: Optional auth handler name used with ``authorization``. + turn_context: Optional TurnContext used with ``authorization``. Returns: - List[MCPServerConfig]: Returns the list of MCP Servers that are configured. + List[MCPServerConfig]: Returns the list of MCP Servers that are configured, + each with an ``Authorization`` header attached when auth context is provided. Raises: ValueError: If required parameters are invalid or empty. - Exception: If there's an error communicating with the tooling gateway. + Exception: If there's an error communicating with the tooling gateway or + a per-audience token exchange fails. """ # Validate input parameters self._validate_input_parameters(agentic_app_id, auth_token) @@ -117,9 +134,17 @@ async def list_tool_servers( # Determine configuration source based on environment if self._is_development_scenario(): - return self._load_servers_from_manifest() + servers = self._load_servers_from_manifest() else: - return await self._load_servers_from_gateway(agentic_app_id, auth_token, options) + servers = await self._load_servers_from_gateway(agentic_app_id, auth_token, options) + + # Acquire per-audience tokens and attach Authorization headers when auth context provided + if authorization is not None and auth_handler_name is not None and turn_context is not None: + servers = await self._attach_per_audience_tokens( + servers, authorization, auth_handler_name, turn_context + ) + + return servers # -------------------------------------------------------------------------- # ENVIRONMENT DETECTION @@ -138,6 +163,72 @@ def _is_development_scenario(self) -> bool: self._logger.debug(f"Environment: {environment}, Development scenario: {is_dev}") return is_dev + async def _attach_per_audience_tokens( + self, + servers: List[MCPServerConfig], + authorization: Authorization, + auth_handler_name: str, + turn_context: TurnContext, + ) -> List[MCPServerConfig]: + """ + Acquire one OAuth token per unique audience and attach an ``Authorization: Bearer`` + header to each server's headers. + + V1 servers (no ``audience`` field, or audience matching the shared ATG AppId) all + share the same ATG-scoped token (one exchange). V2 servers each receive a token + scoped to their own audience GUID. + + Args: + servers: List of MCP server configs returned from discovery. + authorization: Authorization context for token exchange. + auth_handler_name: Auth handler name to pass to the token exchange. + turn_context: TurnContext to pass to the token exchange. + + Returns: + List[MCPServerConfig]: New list of server configs with ``Authorization`` headers set. + + Raises: + Exception: If a token exchange fails for any server. + """ + token_cache: Dict[str, str] = {} # scope → bearer token + result: List[MCPServerConfig] = [] + + for server in servers: + scope = resolve_token_scope_for_server(server) + + if scope not in token_cache: + self._logger.debug( + f"Acquiring token for MCP server '{server.mcp_server_name}' (scope: {scope})" + ) + token_result = await authorization.exchange_token( + turn_context, [scope], auth_handler_name + ) + if token_result is None or not token_result.token: + raise Exception( + f"Failed to obtain token for MCP server '{server.mcp_server_name}'" + f" (scope: {scope})" + ) + token_cache[scope] = token_result.token + + merged_headers: Dict[str, str] = dict(server.headers) if server.headers else {} + merged_headers[Constants.Headers.AUTHORIZATION] = ( + f"{Constants.Headers.BEARER_PREFIX} {token_cache[scope]}" + ) + + result.append( + MCPServerConfig( + mcp_server_name=server.mcp_server_name, + mcp_server_unique_name=server.mcp_server_unique_name, + url=server.url, + headers=merged_headers, + audience=server.audience, + scope=server.scope, + publisher=server.publisher, + ) + ) + + return result + # -------------------------------------------------------------------------- # DEVELOPMENT: MANIFEST-BASED CONFIGURATION # -------------------------------------------------------------------------- @@ -481,6 +572,9 @@ def _parse_manifest_server_config( mcp_server_name=mcp_server_name, mcp_server_unique_name=mcp_server_unique_name, url=final_url, + audience=server_element.get("audience"), + scope=server_element.get("scope"), + publisher=server_element.get("publisher"), ) except Exception: @@ -518,6 +612,9 @@ def _parse_gateway_server_config( mcp_server_name=mcp_server_name, mcp_server_unique_name=mcp_server_unique_name, url=final_url, + audience=server_element.get("audience"), + scope=server_element.get("scope"), + publisher=server_element.get("publisher"), ) except Exception: diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index c552a100..3131676c 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -5,7 +5,13 @@ Provides utility functions for the Tooling components. """ +from __future__ import annotations + import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models.mcp_server_config import MCPServerConfig # Constants for base URLs @@ -17,6 +23,9 @@ PPAPI_TOKEN_SCOPE = "https://api.powerplatform.com" PROD_MCP_PLATFORM_AUTHENTICATION_SCOPE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" +# Shared ATG AppId — V1 servers (no audience field) use this scope +ATG_APP_ID = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + def get_tooling_gateway_for_digital_worker(agentic_app_id: str) -> str: """ @@ -104,3 +113,26 @@ def get_chat_history_endpoint() -> str: str: The chat history endpoint URL. """ return f"{_get_mcp_platform_base_url()}{CHAT_HISTORY_ENDPOINT_PATH}" + + +def resolve_token_scope_for_server(server: MCPServerConfig) -> str: + """ + Resolve the OAuth scope to request for a given MCP server. + + V2 servers carry their own audience GUID in the ``audience`` field and receive + a token scoped to that GUID. V1 servers (no audience, audience equals the shared + ATG AppId, or audience starting with ``api://``) fall back to the shared ATG scope. + + Args: + server: The MCP server configuration to resolve the scope for. + + Returns: + str: The OAuth scope string (e.g. ``"/.default"``). + """ + if ( + server.audience is not None + and server.audience != ATG_APP_ID + and not server.audience.startswith("api://") + ): + return f"{server.audience}/.default" + return f"{ATG_APP_ID}/.default" diff --git a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py index d60085f3..c7324f58 100644 --- a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py @@ -62,6 +62,7 @@ def mock_mcp_server_config(self): config.mcp_server_name = "test-mcp-server" config.mcp_server_unique_name = "test-mcp-server-unique" config.url = "https://test-mcp-server.example.com/api" + config.headers = None # per-audience headers attached by list_tool_servers return config @pytest.fixture @@ -79,8 +80,17 @@ async def test_httpx_client_has_authorization_header( mock_chat_client, mock_mcp_server_config, ): - """Test that httpx.AsyncClient is created with Authorization header.""" + """Test that httpx.AsyncClient is created with Authorization header. + + In the V1/V2 model, list_tool_servers (via _attach_per_audience_tokens) attaches + the per-audience Authorization header to config.headers before returning. The + mock server config must reflect this to verify the header reaches the httpx client. + """ auth_token = "test-bearer-token-xyz" + # Simulate the Authorization header that _attach_per_audience_tokens would attach + mock_mcp_server_config.headers = { + Constants.Headers.AUTHORIZATION: (f"{Constants.Headers.BEARER_PREFIX} {auth_token}") + } with ( patch.object( @@ -481,6 +491,7 @@ async def test_full_client_lifecycle_single_server( mock_server_config.mcp_server_name = "test-server" mock_server_config.mcp_server_unique_name = "test-server-unique" mock_server_config.url = "https://test.example.com/api" + mock_server_config.headers = None # per-audience headers attached by list_tool_servers mock_http_client_instance = MagicMock() @@ -553,16 +564,19 @@ async def test_full_client_lifecycle_multiple_servers( mock_server_config1.mcp_server_name = "server-1" mock_server_config1.mcp_server_unique_name = "server-1-unique" mock_server_config1.url = "https://server1.example.com/api" + mock_server_config1.headers = None # per-audience headers attached by list_tool_servers mock_server_config2 = Mock() mock_server_config2.mcp_server_name = "server-2" mock_server_config2.mcp_server_unique_name = "server-2-unique" mock_server_config2.url = "https://server2.example.com/api" + mock_server_config2.headers = None mock_server_config3 = Mock() mock_server_config3.mcp_server_name = "server-3" mock_server_config3.mcp_server_unique_name = "server-3-unique" mock_server_config3.url = "https://server3.example.com/api" + mock_server_config3.headers = None # Create unique mock clients for each server mock_clients = [MagicMock() for _ in range(3)] @@ -661,6 +675,7 @@ async def test_cleanup_called_twice_after_creating_clients( mock_server_config.mcp_server_name = "test-server" mock_server_config.mcp_server_unique_name = "test-server-unique" mock_server_config.url = "https://test.example.com/api" + mock_server_config.headers = None # per-audience headers attached by list_tool_servers mock_http_client_instance = MagicMock() From d84c41d3f85034737a1485a37c7b244a48a9ac4c Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Fri, 3 Apr 2026 15:27:41 +0530 Subject: [PATCH 02/19] Extend V1/V2 per-audience token support to all SDK extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../services/mcp_tool_registration_service.py | 17 +- .../openai/mcp_tool_registration_service.py | 21 +- .../services/mcp_tool_registration_service.py | 19 +- .../tooling/utils/utility.py | 24 +- .../test_mcp_tool_registration_service.py | 3 +- .../tooling/test_mcp_server_configuration.py | 252 ++++++++++++++++++ 6 files changed, 305 insertions(+), 31 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py index 5c5f6385..10207012 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py @@ -89,6 +89,9 @@ async def add_tool_servers_to_agent( 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") @@ -104,10 +107,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 @@ -119,10 +118,18 @@ 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 {} + 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, ) ) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py index dc4893fb..90c80637 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py @@ -104,6 +104,9 @@ async def add_tool_servers_to_agent( 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") @@ -118,6 +121,7 @@ async def add_tool_servers_to_agent( server_info = MCPServerInfo( name=server_name, url=server_url, + headers=dict(server_config.headers) if server_config.headers else None, ) mcp_servers_info.append(server_info) @@ -149,16 +153,15 @@ async def add_tool_servers_to_agent( if si.url not in existing_server_urls: try: - # Prepare headers with authorization - headers = si.headers or {} - if auth_token: - headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + # Merge base headers with per-server headers from list_tool_servers. + # si.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(si.headers) if si.headers else {} + headers = {**base_headers, **server_headers} # per-audience token takes precedence # Create MCPServerStreamableHttpParams with proper configuration params = MCPServerStreamableHttpParams(url=si.url, headers=headers) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py index b4274081..de8f26e4 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py @@ -119,22 +119,23 @@ async def add_tool_servers_to_agent( # Get and process servers options = ToolOptions(orchestrator_name=self._orchestrator_name) servers = await self._mcp_server_configuration_service.list_tool_servers( - agentic_app_id, auth_token, options + agentic_app_id, auth_token, options, + authorization=auth, + auth_handler_name=auth_handler_name, + turn_context=context, ) self._logger.info(f"🔧 Adding MCP tools from {len(servers)} servers") # Process each server (matching C# foreach pattern) for server in servers: try: - headers = { - Constants.Headers.AUTHORIZATION: ( - f"{Constants.Headers.BEARER_PREFIX} {auth_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(server.headers) if server.headers else {} + headers = {**base_headers, **server_headers} # per-audience token takes precedence # Use the URL from server (always populated by the configuration service) server_url = server.url diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index 3131676c..eae78f63 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -15,7 +15,7 @@ # Constants for base URLs -MCP_PLATFORM_PROD_BASE_URL = "https://agent365.svc.cloud.microsoft" +MCP_PLATFORM_PROD_BASE_URL = "https://test.agent365.svc.cloud.dev.microsoft" # API endpoint paths CHAT_HISTORY_ENDPOINT_PATH = "/agents/real-time-threat-protection/chat-message" @@ -25,6 +25,8 @@ # Shared ATG AppId — V1 servers (no audience field) use this scope ATG_APP_ID = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" +# ATG AppId in Application ID URI form — also treated as V1 +ATG_APP_ID_URI = f"api://{ATG_APP_ID}" def get_tooling_gateway_for_digital_worker(agentic_app_id: str) -> str: @@ -38,7 +40,7 @@ def get_tooling_gateway_for_digital_worker(agentic_app_id: str) -> str: str: The tooling gateway URL for the digital worker. """ # The endpoint needs to be updated based on the environment (prod, dev, etc.) - return f"{_get_mcp_platform_base_url()}/agents/{agentic_app_id}/mcpServers" + return f"{_get_mcp_platform_base_url()}/agents/v2/{agentic_app_id}/mcpServers" def get_mcp_base_url() -> str: @@ -119,20 +121,28 @@ def resolve_token_scope_for_server(server: MCPServerConfig) -> str: """ Resolve the OAuth scope to request for a given MCP server. - V2 servers carry their own audience GUID in the ``audience`` field and receive - a token scoped to that GUID. V1 servers (no audience, audience equals the shared - ATG AppId, or audience starting with ``api://``) fall back to the shared ATG scope. + V2 servers carry their own audience in the ``audience`` field (bare GUID or + ``api://`` URI form). When an explicit ``scope`` is provided (e.g. + ``"Tools.ListInvoke.All"``), the scope is ``{audience}/{scope}``. When scope + is absent, ``{audience}/.default`` is used (relies on pre-consented scopes). + V1 servers (no audience, audience equals the shared ATG AppId in bare GUID or + ``api://`` URI form) always fall back to the shared ATG ``/.default`` scope. Args: server: The MCP server configuration to resolve the scope for. Returns: - str: The OAuth scope string (e.g. ``"/.default"``). + str: The OAuth scope string, e.g. ``"/Tools.ListInvoke.All"``, + ``"api:///.default"``, or the shared ATG ``"/.default"``. """ if ( server.audience is not None and server.audience != ATG_APP_ID - and not server.audience.startswith("api://") + and server.audience != ATG_APP_ID_URI ): + # V2: use explicit scope when present, fall back to /.default (pre-consented) + if server.scope: + return f"{server.audience}/{server.scope}" return f"{server.audience}/.default" + # V1: shared ATG platform token return f"{ATG_APP_ID}/.default" diff --git a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py index c7324f58..5066c0a1 100644 --- a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py @@ -57,12 +57,13 @@ def mock_chat_client(self): @pytest.fixture def mock_mcp_server_config(self): - """Create a mock MCP server configuration.""" + """Create a mock V2 MCP server configuration (has GUID audience for per-audience token).""" config = Mock() config.mcp_server_name = "test-mcp-server" config.mcp_server_unique_name = "test-mcp-server-unique" config.url = "https://test-mcp-server.example.com/api" config.headers = None # per-audience headers attached by list_tool_servers + config.audience = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # V2: GUID audience return config @pytest.fixture diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 619f51ec..5901eec6 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -208,6 +208,7 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u "mcpServerName": "ProdServer", "mcpServerUniqueName": "prod_server", "url": "https://prod.custom.url/mcp", + "audience": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", } ] } @@ -241,6 +242,257 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u assert servers[0].mcp_server_name == "ProdServer" assert servers[0].mcp_server_unique_name == "prod_server" assert servers[0].url == "https://prod.custom.url/mcp" + assert servers[0].audience == "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + + +class TestResolveTokenScopeForServer: + """Tests for resolve_token_scope_for_server() utility function.""" + + PROD_ATG_APP_ID = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + TEST_ATG_APP_ID = "05879165-0320-489e-b644-f72b33f3edf0" + V2_GUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + + def _make_server( + self, audience: str | None = None, scope: str | None = None + ) -> MCPServerConfig: + return MCPServerConfig( + mcp_server_name="TestServer", + mcp_server_unique_name="test_server", + audience=audience, + scope=scope, + ) + + # ------------------------------------------------------------------ + # V1 scenarios — all fall back to shared ATG scope + # ------------------------------------------------------------------ + + def test_v1_no_audience_returns_atg_scope(self): + """V1: no audience → shared ATG /.default.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience=None, scope=None) + assert resolve_token_scope_for_server(server) == f"{self.PROD_ATG_APP_ID}/.default" + + def test_v1_atg_guid_audience_falls_back_to_atg_scope(self): + """V1: audience == ATG AppId bare GUID → shared ATG /.default.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience=self.PROD_ATG_APP_ID, scope="McpServers.Teams.All") + assert resolve_token_scope_for_server(server) == f"{self.PROD_ATG_APP_ID}/.default" + + def test_v1_atg_audience_in_uri_form_falls_back_to_atg_scope(self): + """V1: audience == api:// → shared ATG /.default.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience=f"api://{self.PROD_ATG_APP_ID}") + assert resolve_token_scope_for_server(server) == f"{self.PROD_ATG_APP_ID}/.default" + + # ------------------------------------------------------------------ + # V1 test-env — ATG_APP_ID overridden via MCP_PLATFORM_APP_ID + # ------------------------------------------------------------------ + + def test_v1_test_env_shared_audience_not_treated_as_v2(self): + """V1 test env: test audience GUID is a different GUID from prod ATG — treated as V2 + unless the caller configures ATG_APP_ID to match. This test documents that the SDK + uses the prod ATG_APP_ID constant; test environments must set MCP_PLATFORM_APP_ID + via the CLI / .env so gateway returns the correct shared audience in discovery.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + # test env audience is a different GUID — with prod ATG_APP_ID hardcoded, + # it is treated as V2 (resolved to its own /.default scope). + # This is intentional: V2 logic handles it correctly since Tools.ListInvoke.All + # is pre-consented on the test app registration. + server = self._make_server(audience=self.TEST_ATG_APP_ID, scope=None) + assert resolve_token_scope_for_server(server) == f"{self.TEST_ATG_APP_ID}/.default" + + # ------------------------------------------------------------------ + # V2 scenarios — unique audience, explicit scope + # ------------------------------------------------------------------ + + def test_v2_guid_audience_with_explicit_scope(self): + """V2: unique GUID audience + explicit scope → /.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience=self.V2_GUID, scope="Tools.ListInvoke.All") + assert resolve_token_scope_for_server(server) == f"{self.V2_GUID}/Tools.ListInvoke.All" + + def test_v2_api_uri_audience_with_explicit_scope(self): + """V2: api:// audience + explicit scope → api:///.""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server( + audience="api://mcp-calendartools", scope="McpServers.Calendar.All" + ) + assert resolve_token_scope_for_server(server) == "api://mcp-calendartools/McpServers.Calendar.All" + + def test_v2_guid_audience_null_scope_falls_back_to_default(self): + """V2: unique GUID audience + null scope → /.default (pre-consented).""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience=self.V2_GUID, scope=None) + assert resolve_token_scope_for_server(server) == f"{self.V2_GUID}/.default" + + def test_v2_api_uri_audience_null_scope_falls_back_to_default(self): + """V2: api:// audience + null scope → api:///.default (pre-consented).""" + from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server + + server = self._make_server(audience="api://mcp-mailtools", scope=None) + assert resolve_token_scope_for_server(server) == "api://mcp-mailtools/.default" + + +class TestAttachPerAudienceTokens: + """Tests for McpToolServerConfigurationService._attach_per_audience_tokens().""" + + ATG_APP_ID = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + + @pytest.fixture + def service(self): + return McpToolServerConfigurationService() + + def _make_server(self, name: str, audience: str | None = None) -> MCPServerConfig: + return MCPServerConfig( + mcp_server_name=name, + mcp_server_unique_name=name.lower(), + url=f"https://{name}.example.com/mcp", + audience=audience, + ) + + def _make_auth_context(self, token: str = "tok"): + authorization = MagicMock() + token_result = MagicMock() + token_result.token = token + authorization.exchange_token = AsyncMock(return_value=token_result) + turn_context = MagicMock() + return authorization, turn_context + + @pytest.mark.asyncio + async def test_v1_server_gets_atg_token(self, service): + """V1 server (no audience) receives ATG-scoped token.""" + servers = [self._make_server("mail")] + authorization, turn_context = self._make_auth_context("atg-token") + + result = await service._attach_per_audience_tokens( + servers, authorization, "handler", turn_context + ) + + assert len(result) == 1 + assert result[0].headers["Authorization"] == "Bearer atg-token" + authorization.exchange_token.assert_called_once_with( + turn_context, [f"{self.ATG_APP_ID}/.default"], "handler" + ) + + @pytest.mark.asyncio + async def test_v2_server_gets_per_audience_token(self, service): + """V2 server gets token scoped to its own audience GUID.""" + guid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + servers = [self._make_server("calendar", audience=guid)] + authorization, turn_context = self._make_auth_context("v2-token") + + result = await service._attach_per_audience_tokens( + servers, authorization, "handler", turn_context + ) + + assert result[0].headers["Authorization"] == "Bearer v2-token" + authorization.exchange_token.assert_called_once_with( + turn_context, [f"{guid}/.default"], "handler" + ) + + @pytest.mark.asyncio + async def test_multiple_v1_servers_share_one_token_exchange(self, service): + """Multiple V1 servers deduplicate to a single token exchange.""" + servers = [ + self._make_server("mail"), + self._make_server("calendar"), + self._make_server("files"), + ] + authorization, turn_context = self._make_auth_context("shared-atg-token") + + result = await service._attach_per_audience_tokens( + servers, authorization, "handler", turn_context + ) + + assert all(s.headers["Authorization"] == "Bearer shared-atg-token" for s in result) + authorization.exchange_token.assert_called_once() + + @pytest.mark.asyncio + async def test_mixed_v1_v2_servers_deduplicate_by_scope(self, service): + """Mixed V1/V2 list: one exchange per unique scope.""" + guid1 = "aaaaaaaa-0000-0000-0000-000000000001" + guid2 = "bbbbbbbb-0000-0000-0000-000000000002" + servers = [ + self._make_server("mail"), # V1 + self._make_server("cal1", guid1), # V2 guid1 + self._make_server("cal2", guid2), # V2 guid2 + self._make_server("files"), # V1 (same scope as mail → no 2nd exchange) + self._make_server("cal3", guid1), # V2 guid1 again → no 2nd exchange + ] + + authorization = MagicMock() + call_count = [0] + + async def fake_exchange(ctx, scopes, handler): + call_count[0] += 1 + result = MagicMock() + result.token = f"token-for-{scopes[0]}" + return result + + authorization.exchange_token = fake_exchange + turn_context = MagicMock() + + result = await service._attach_per_audience_tokens( + servers, authorization, "handler", turn_context + ) + + assert call_count[0] == 3 # ATG + guid1 + guid2 + assert result[0].headers["Authorization"] == f"Bearer token-for-{self.ATG_APP_ID}/.default" + assert result[1].headers["Authorization"] == f"Bearer token-for-{guid1}/.default" + assert result[2].headers["Authorization"] == f"Bearer token-for-{guid2}/.default" + assert result[3].headers["Authorization"] == f"Bearer token-for-{self.ATG_APP_ID}/.default" + assert result[4].headers["Authorization"] == f"Bearer token-for-{guid1}/.default" + + @pytest.mark.asyncio + async def test_raises_when_token_exchange_returns_none(self, service): + """Exception raised when token exchange returns None.""" + servers = [self._make_server("mail")] + authorization = MagicMock() + authorization.exchange_token = AsyncMock(return_value=None) + + with pytest.raises(Exception, match="Failed to obtain token"): + await service._attach_per_audience_tokens( + servers, authorization, "handler", MagicMock() + ) + + @pytest.mark.asyncio + async def test_raises_when_token_is_empty(self, service): + """Exception raised when token result has empty token string.""" + servers = [self._make_server("mail")] + authorization = MagicMock() + token_result = MagicMock() + token_result.token = "" + authorization.exchange_token = AsyncMock(return_value=token_result) + + with pytest.raises(Exception, match="Failed to obtain token"): + await service._attach_per_audience_tokens( + servers, authorization, "handler", MagicMock() + ) + + @pytest.mark.asyncio + async def test_preserves_existing_server_headers(self, service): + """Existing server headers are preserved alongside the new Authorization header.""" + server = MCPServerConfig( + mcp_server_name="TestServer", + mcp_server_unique_name="test_server", + url="https://test.example.com/mcp", + headers={"X-Custom": "my-value"}, + ) + authorization, turn_context = self._make_auth_context("tok") + + result = await service._attach_per_audience_tokens( + [server], authorization, "handler", turn_context + ) + + assert result[0].headers["X-Custom"] == "my-value" + assert result[0].headers["Authorization"] == "Bearer tok" class TestPrepareGatewayHeaders: From 74451d762464e25ead78c3dca19420a94a5fd827 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Fri, 3 Apr 2026 16:05:18 +0530 Subject: [PATCH 03/19] Extend V1/V2 per-audience token support to all SDK extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../openai/mcp_tool_registration_service.py | 5 ++++- .../services/mcp_tool_registration_service.py | 4 +++- .../microsoft-agents-a365-tooling/CHANGELOG.md | 16 ++++++++++++++++ tests/tooling/test_mcp_server_configuration.py | 15 +++++++++------ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py index 90c80637..4d5a751e 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py @@ -161,7 +161,10 @@ async def add_tool_servers_to_agent( ) } server_headers = dict(si.headers) if si.headers else {} - headers = {**base_headers, **server_headers} # per-audience token takes precedence + headers = { + **base_headers, + **server_headers, + } # per-audience token takes precedence # Create MCPServerStreamableHttpParams with proper configuration params = MCPServerStreamableHttpParams(url=si.url, headers=headers) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py index de8f26e4..b908720c 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py @@ -119,7 +119,9 @@ async def add_tool_servers_to_agent( # Get and process servers options = ToolOptions(orchestrator_name=self._orchestrator_name) servers = await self._mcp_server_configuration_service.list_tool_servers( - agentic_app_id, auth_token, options, + agentic_app_id, + auth_token, + options, authorization=auth, auth_handler_name=auth_handler_name, turn_context=context, diff --git a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md index a6a769c7..a1a14d5a 100644 --- a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md +++ b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added V1/V2 per-audience token acquisition support in `McpToolServerConfigurationService.list_tool_servers()`. When `authorization`, `auth_handler_name`, and `turn_context` are provided, each MCP server receives its own OAuth token scoped to its audience — V1 servers (no audience, or shared ATG AppId) share a single ATG-scoped token; V2 servers (unique non-ATG audience GUID or `api://` URI) each receive a token scoped to `{audience}/{scope}` (or `{audience}/.default` when scope is absent and pre-consented) +- Added `_attach_per_audience_tokens()` private method to `McpToolServerConfigurationService` — acquires one token per unique scope, caches within the call to avoid redundant exchanges, and attaches `Authorization: Bearer` headers to each server config +- Added `resolve_token_scope_for_server()` utility function to derive the correct OAuth scope for a given `MCPServerConfig` based on its `audience` and `scope` fields +- Added `audience`, `scope`, `publisher`, and `headers` fields to `MCPServerConfig` +- Gateway discovery endpoint bumped to `/agents/v2/{id}/mcpServers` +- `_parse_gateway_server_config()` and `_parse_manifest_server_config()` now map `audience`, `scope`, and `publisher` fields from gateway/manifest responses into `MCPServerConfig` + +### Changed + +- OpenAI, Semantic Kernel, and Google ADK extensions now pass auth context to `list_tool_servers()` and merge per-server headers (`{**base_headers, **server.headers}`) instead of injecting a single shared ATG token for all servers — fully backward compatible, V1 agents continue to receive the same shared ATG token + +### Notes + +- **Backward compatible**: agents with V1 manifests (null audience or shared ATG AppId) work identically with the new SDK — no token exchange behaviour changes +- **Migration required for V2**: agents upgraded to V2 blueprint permissions (per-audience MCP servers) require this SDK version. Running a V2 blueprint with the old SDK will result in MCP tool auth failures (401/403) + - Added `send_chat_history` method to `McpToolServerConfigurationService` for sending chat conversation history to the MCP platform for real-time threat protection analysis - Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history - Added `ChatMessageRequest` Pydantic model for the chat history API request payload diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 5901eec6..bb769ae2 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -323,7 +323,10 @@ def test_v2_api_uri_audience_with_explicit_scope(self): server = self._make_server( audience="api://mcp-calendartools", scope="McpServers.Calendar.All" ) - assert resolve_token_scope_for_server(server) == "api://mcp-calendartools/McpServers.Calendar.All" + assert ( + resolve_token_scope_for_server(server) + == "api://mcp-calendartools/McpServers.Calendar.All" + ) def test_v2_guid_audience_null_scope_falls_back_to_default(self): """V2: unique GUID audience + null scope → /.default (pre-consented).""" @@ -420,11 +423,11 @@ async def test_mixed_v1_v2_servers_deduplicate_by_scope(self, service): guid1 = "aaaaaaaa-0000-0000-0000-000000000001" guid2 = "bbbbbbbb-0000-0000-0000-000000000002" servers = [ - self._make_server("mail"), # V1 - self._make_server("cal1", guid1), # V2 guid1 - self._make_server("cal2", guid2), # V2 guid2 - self._make_server("files"), # V1 (same scope as mail → no 2nd exchange) - self._make_server("cal3", guid1), # V2 guid1 again → no 2nd exchange + self._make_server("mail"), # V1 + self._make_server("cal1", guid1), # V2 guid1 + self._make_server("cal2", guid2), # V2 guid2 + self._make_server("files"), # V1 (same scope as mail → no 2nd exchange) + self._make_server("cal3", guid1), # V2 guid1 again → no 2nd exchange ] authorization = MagicMock() From f3b76d42664992af7651b1558647a39651f44abc Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 9 Apr 2026 20:33:14 +0530 Subject: [PATCH 04/19] Fix manifest/gateway parsing and add local dev per-server token support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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_ 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 --- .../CHANGELOG.md | 8 ++- .../mcp_tool_server_configuration_service.py | 64 +++++++++++++++++-- .../tooling/utils/utility.py | 1 + 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md index a1a14d5a..3ed97c6e 100644 --- a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md +++ b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md @@ -9,21 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added V1/V2 per-audience token acquisition support in `McpToolServerConfigurationService.list_tool_servers()`. When `authorization`, `auth_handler_name`, and `turn_context` are provided, each MCP server receives its own OAuth token scoped to its audience — V1 servers (no audience, or shared ATG AppId) share a single ATG-scoped token; V2 servers (unique non-ATG audience GUID or `api://` URI) each receive a token scoped to `{audience}/{scope}` (or `{audience}/.default` when scope is absent and pre-consented) +- Added MCP V1/V2 per-audience token acquisition support in `McpToolServerConfigurationService.list_tool_servers()`. When `authorization`, `auth_handler_name`, and `turn_context` are provided, each MCP server receives its own OAuth token scoped to its audience — V1 servers (no audience, or shared ATG AppId) share a single ATG-scoped token; V2 servers (unique non-ATG audience GUID or `api://` URI) each receive a token scoped to `{audience}/{scope}` (or `{audience}/.default` when scope is absent and pre-consented) - Added `_attach_per_audience_tokens()` private method to `McpToolServerConfigurationService` — acquires one token per unique scope, caches within the call to avoid redundant exchanges, and attaches `Authorization: Bearer` headers to each server config - Added `resolve_token_scope_for_server()` utility function to derive the correct OAuth scope for a given `MCPServerConfig` based on its `audience` and `scope` fields - Added `audience`, `scope`, `publisher`, and `headers` fields to `MCPServerConfig` - Gateway discovery endpoint bumped to `/agents/v2/{id}/mcpServers` - `_parse_gateway_server_config()` and `_parse_manifest_server_config()` now map `audience`, `scope`, and `publisher` fields from gateway/manifest responses into `MCPServerConfig` +- Added `_attach_dev_tokens()` private method to `McpToolServerConfigurationService` — reads `BEARER_TOKEN_` and `BEARER_TOKEN` environment variables written by the `a365 develop get-token` CLI and attaches per-server `Authorization: Bearer` headers during local dev manifest loading; no-op in production + ### Changed - OpenAI, Semantic Kernel, and Google ADK extensions now pass auth context to `list_tool_servers()` and merge per-server headers (`{**base_headers, **server.headers}`) instead of injecting a single shared ATG token for all servers — fully backward compatible, V1 agents continue to receive the same shared ATG token +- `_extract_server_unique_name()` now falls back to `mcpServerName` when `mcpServerUniqueName` is absent from the manifest or gateway response +- `_parse_manifest_server_config()` and `_parse_gateway_server_config()` now normalize `"null"` scope strings and `"default"` audience strings to `None` to prevent incorrect V2 token scope resolution +- `resolve_token_scope_for_server()` now treats `"default"` audience as V1 (shared ATG token) as a defense-in-depth guard ### Notes - **Backward compatible**: agents with V1 manifests (null audience or shared ATG AppId) work identically with the new SDK — no token exchange behaviour changes - **Migration required for V2**: agents upgraded to V2 blueprint permissions (per-audience MCP servers) require this SDK version. Running a V2 blueprint with the old SDK will result in MCP tool auth failures (401/403) +- **Local dev token flow**: run `a365 develop get-token` before starting the agent locally; the CLI writes `BEARER_TOKEN` (V1 shared) and `BEARER_TOKEN_` (V2 per-server) to the environment, which the SDK reads automatically from the manifest path - Added `send_chat_history` method to `McpToolServerConfigurationService` for sending chat conversation history to the MCP platform for real-time threat protection analysis - Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index bd47940c..5bf11f6f 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -267,6 +267,7 @@ def _load_servers_from_manifest(self) -> List[MCPServerConfig]: if manifest_path and manifest_path.exists(): self._logger.info(f"Loading MCP servers from: {manifest_path}") mcp_servers = self._parse_manifest_file(manifest_path) + self._attach_dev_tokens(mcp_servers) else: self._log_manifest_search_failure() @@ -568,12 +569,18 @@ def _parse_manifest_server_config( # Determine the final URL: use custom URL if provided, otherwise construct it final_url = endpoint if endpoint else build_mcp_server_url(server_name) + scope_raw = server_element.get("scope") + scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw + + audience_raw = server_element.get("audience") + audience = None if not audience_raw or audience_raw.lower() == "default" else audience_raw + return MCPServerConfig( mcp_server_name=mcp_server_name, mcp_server_unique_name=mcp_server_unique_name, url=final_url, - audience=server_element.get("audience"), - scope=server_element.get("scope"), + audience=audience, + scope=scope, publisher=server_element.get("publisher"), ) @@ -608,12 +615,18 @@ def _parse_gateway_server_config( # Determine the final URL: use custom URL if provided, otherwise construct it final_url = endpoint if endpoint else build_mcp_server_url(server_name) + scope_raw = server_element.get("scope") + scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw + + audience_raw = server_element.get("audience") + audience = None if not audience_raw or audience_raw.lower() == "default" else audience_raw + return MCPServerConfig( mcp_server_name=mcp_server_name, mcp_server_unique_name=mcp_server_unique_name, url=final_url, - audience=server_element.get("audience"), - scope=server_element.get("scope"), + audience=audience, + scope=scope, publisher=server_element.get("publisher"), ) @@ -624,6 +637,46 @@ def _parse_gateway_server_config( # VALIDATION AND UTILITY HELPERS # -------------------------------------------------------------------------- + def _attach_dev_tokens(self, servers: List[MCPServerConfig]) -> None: + """ + Attach per-server Authorization headers from environment variables (local dev only). + + The CLI (``a365 develop get-token``) pre-acquires tokens interactively and writes + them to the environment before the agent starts: + + - ``BEARER_TOKEN_`` — V2 per-server token + - ``BEARER_TOKEN`` — V1 shared ATG token (fallback) + + For each server, the resolution order is: + 1. ``BEARER_TOKEN_`` (per-audience V2 token) + 2. ``BEARER_TOKEN`` (shared V1 ATG token) + + If neither is set, no Authorization header is injected and the server is left as-is. + This method is a no-op in production (``_load_servers_from_manifest`` is never called + there) and when ``authorization`` is provided (``_attach_per_audience_tokens`` takes + precedence via the caller in ``list_tool_servers``). + + Args: + servers: List of MCP server configs parsed from ToolingManifest.json. + """ + shared_token = os.getenv("BEARER_TOKEN") + + for server in servers: + unique_name = server.mcp_server_unique_name or "" + per_server_token = os.getenv(f"BEARER_TOKEN_{unique_name.upper()}") + token = per_server_token or shared_token + + if token: + existing = dict(server.headers) if server.headers else {} + existing[Constants.Headers.AUTHORIZATION] = ( + f"{Constants.Headers.BEARER_PREFIX} {token}" + ) + server.headers = existing + self._logger.debug( + f"Attached {'per-server' if per_server_token else 'shared'} " + f"dev token for '{server.mcp_server_unique_name}'" + ) + def _validate_input_parameters(self, agentic_app_id: str, auth_token: str) -> None: """ Validates input parameters for the main API method. @@ -668,6 +721,9 @@ def _extract_server_unique_name(self, server_element: Dict[str, Any]) -> Optiona server_element["mcpServerUniqueName"], str ): return server_element["mcpServerUniqueName"] + # Fall back to mcpServerName when mcpServerUniqueName is absent + if "mcpServerName" in server_element and isinstance(server_element["mcpServerName"], str): + return server_element["mcpServerName"] return None def _extract_server_url(self, server_element: Dict[str, Any]) -> Optional[str]: diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index eae78f63..9de4e8f1 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -137,6 +137,7 @@ def resolve_token_scope_for_server(server: MCPServerConfig) -> str: """ if ( server.audience is not None + and server.audience.lower() != "default" and server.audience != ATG_APP_ID and server.audience != ATG_APP_ID_URI ): From b1f6a3b29ea0000a910d383a06633639d8a8fe4d Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 9 Apr 2026 20:57:15 +0530 Subject: [PATCH 05/19] fixed formatting --- .../services/mcp_tool_server_configuration_service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 5bf11f6f..0335c8d7 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -573,7 +573,9 @@ def _parse_manifest_server_config( scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw audience_raw = server_element.get("audience") - audience = None if not audience_raw or audience_raw.lower() == "default" else audience_raw + audience = ( + None if not audience_raw or audience_raw.lower() == "default" else audience_raw + ) return MCPServerConfig( mcp_server_name=mcp_server_name, @@ -619,7 +621,9 @@ def _parse_gateway_server_config( scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw audience_raw = server_element.get("audience") - audience = None if not audience_raw or audience_raw.lower() == "default" else audience_raw + audience = ( + None if not audience_raw or audience_raw.lower() == "default" else audience_raw + ) return MCPServerConfig( mcp_server_name=mcp_server_name, From 4cb88c908cf71d4eab971207c54c47d525d0d8d8 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Sun, 12 Apr 2026 13:01:29 +0530 Subject: [PATCH 06/19] =?UTF-8?q?mcp=5Ftool=5Fregistration=5Fservice.py=20?= =?UTF-8?q?=E2=80=94=20threaded=20auth,=20auth=5Fhandler=5Fname,=20context?= =?UTF-8?q?=20from=20add=5Ftool=5Fservers=5Fto=5Fagent=20into=20=5Fget=5Fm?= =?UTF-8?q?cp=5Ftool=5Fdefinitions=5Fand=5Fresources,=20which=20now=20pass?= =?UTF-8?q?es=20them=20as=20keyword=20args=20to=20list=5Ftool=5Fservers()?= =?UTF-8?q?=20so=20=5Fattach=5Fper=5Faudience=5Ftokens()=20runs=20for=20V2?= =?UTF-8?q?=20servers.=20Header=20injection=20reads=20server.headers=20(pe?= =?UTF-8?q?r-audience=20token)=20with=20fallback=20to=20the=20shared=20aut?= =?UTF-8?q?h=5Ftoken.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../services/mcp_tool_registration_service.py | 40 +- .../test_mcp_tool_registration_service.py | 386 ++++++++++++++++++ 2 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py diff --git a/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py index c235996a..04c56c13 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py @@ -106,9 +106,10 @@ async def add_tool_servers_to_agent( try: agentic_app_id = Utility.resolve_agent_identity(context, auth_token) - # Get the tool definitions and resources using the async implementation + # 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 @@ -127,7 +128,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. @@ -136,7 +142,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. @@ -145,11 +154,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( @@ -191,14 +206,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) diff --git a/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py new file mode 100644 index 00000000..2a17a314 --- /dev/null +++ b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py @@ -0,0 +1,386 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for add_tool_servers_to_agent in the AzureAIFoundry McpToolRegistrationService. + +These tests verify that: +- list_tool_servers() is called with the full authorization context so that + _attach_per_audience_tokens() runs and V2 per-audience tokens are used. +- Each McpTool receives the per-server Authorization header from server.headers, + not the shared discovery auth_token. +- The User-Agent header is set on every McpTool. +- The fallback to the shared auth_token works when server.headers has no + Authorization (defensive path). +""" + +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch + +import pytest + +from microsoft_agents_a365.tooling.extensions.azureaifoundry.services import ( + McpToolRegistrationService, +) +from microsoft_agents_a365.tooling.utils.constants import Constants + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + +_MODULE = "microsoft_agents_a365.tooling.extensions.azureaifoundry.services.mcp_tool_registration_service" + + +def _make_mock_server(auth_header: str | None = None): + """Create a minimal MCPServerConfig mock with optional Authorization header.""" + server = Mock() + server.mcp_server_name = "mcp_TestServer" + server.mcp_server_unique_name = "test_server" + server.url = "https://test-mcp.example.com/mcp" + server.headers = ( + {Constants.Headers.AUTHORIZATION: auth_header} if auth_header is not None else {} + ) + return server + + +def _make_mock_mcp_tool(): + """Create a minimal McpTool mock that records update_headers calls.""" + tool = MagicMock() + tool.definitions = [Mock()] + tool.resources = None + return tool + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def mock_turn_context(): + context = Mock() + context.activity = Mock() + context.activity.conversation = Mock() + context.activity.conversation.id = "conv-123" + context.activity.id = "msg-456" + context.activity.text = "hello" + return context + + +@pytest.fixture +def mock_auth(): + auth = AsyncMock() + token_result = Mock() + token_result.token = "discovery-token" + auth.exchange_token = AsyncMock(return_value=token_result) + return auth + + +@pytest.fixture +def mock_project_client(): + client = Mock() + client.agents = Mock() + client.agents.update_agent = Mock() + return client + + +@pytest.fixture +def service(): + svc = McpToolRegistrationService() + svc._mcp_server_configuration_service = Mock() + return svc + + +# --------------------------------------------------------------------------- +# Tests — auth context forwarded to list_tool_servers +# --------------------------------------------------------------------------- + + +class TestAuthContextForwardedToListToolServers: + """Verify list_tool_servers is called with authorization/auth_handler_name/turn_context.""" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_list_tool_servers_receives_auth_context( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """list_tool_servers must receive the full auth context so per-audience + token exchange runs for V2 MCP servers.""" + auth_token = "discovery-token" + mock_server = _make_mock_server(f"Bearer per-audience-token") + mock_mcp_tool = _make_mock_mcp_tool() + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[mock_server] + ) + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="AzureAIFoundry/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="test-handler", + context=mock_turn_context, + auth_token=auth_token, + ) + + service._mcp_server_configuration_service.list_tool_servers.assert_awaited_once_with( + "test-aai", + auth_token, + ANY, # ToolOptions — any value + authorization=mock_auth, + auth_handler_name="test-handler", + turn_context=mock_turn_context, + ) + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_list_tool_servers_called_with_correct_keyword_args( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """Verify keyword argument names match list_tool_servers signature exactly.""" + auth_token = "token-xyz" + mock_server = _make_mock_server(f"Bearer v2-token") + mock_mcp_tool = _make_mock_mcp_tool() + + list_tool_servers_mock = AsyncMock(return_value=[mock_server]) + service._mcp_server_configuration_service.list_tool_servers = list_tool_servers_mock + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="aai-123"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="UA/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler-name", + context=mock_turn_context, + auth_token=auth_token, + ) + + _, kwargs = list_tool_servers_mock.call_args + assert kwargs["authorization"] is mock_auth + assert kwargs["auth_handler_name"] == "handler-name" + assert kwargs["turn_context"] is mock_turn_context + + +# --------------------------------------------------------------------------- +# Tests — per-server Authorization header on McpTool +# --------------------------------------------------------------------------- + + +class TestPerServerAuthorizationHeader: + """Verify McpTool receives the per-audience token from server.headers, not + the shared discovery auth_token.""" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_mcp_tool_uses_per_server_auth_header_not_shared_token( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """McpTool.update_headers must be called with the per-audience token + attached by _attach_per_audience_tokens(), not the shared discovery token.""" + discovery_token = "shared-atg-discovery-token" + per_audience_token = "Bearer per-audience-v2-token" + + mock_server = _make_mock_server(auth_header=per_audience_token) + mock_mcp_tool = _make_mock_mcp_tool() + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[mock_server] + ) + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="UA/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler", + context=mock_turn_context, + auth_token=discovery_token, + ) + + auth_calls = [ + call + for call in mock_mcp_tool.update_headers.call_args_list + if call.args[0] == Constants.Headers.AUTHORIZATION + ] + assert len(auth_calls) == 1 + assert auth_calls[0].args[1] == per_audience_token + # Must NOT use the shared discovery token + assert auth_calls[0].args[1] != f"Bearer {discovery_token}" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_mcp_tool_falls_back_to_shared_token_when_server_headers_empty( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """When server.headers has no Authorization (dev manifest path or empty), + fall back to wrapping auth_token as Bearer.""" + auth_token = "fallback-token" + mock_server = _make_mock_server(auth_header=None) # no header + mock_mcp_tool = _make_mock_mcp_tool() + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[mock_server] + ) + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="UA/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler", + context=mock_turn_context, + auth_token=auth_token, + ) + + auth_calls = [ + call + for call in mock_mcp_tool.update_headers.call_args_list + if call.args[0] == Constants.Headers.AUTHORIZATION + ] + assert len(auth_calls) == 1 + assert auth_calls[0].args[1] == f"Bearer {auth_token}" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_mcp_tool_does_not_double_prefix_bearer_token( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """If auth_token already starts with 'Bearer ', it must not be double-prefixed.""" + auth_token = "Bearer already-prefixed-token" + mock_server = _make_mock_server(auth_header=None) + mock_mcp_tool = _make_mock_mcp_tool() + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[mock_server] + ) + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="UA/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler", + context=mock_turn_context, + auth_token=auth_token, + ) + + auth_calls = [ + call + for call in mock_mcp_tool.update_headers.call_args_list + if call.args[0] == Constants.Headers.AUTHORIZATION + ] + assert len(auth_calls) == 1 + assert auth_calls[0].args[1] == auth_token # unchanged + + +# --------------------------------------------------------------------------- +# Tests — User-Agent header +# --------------------------------------------------------------------------- + + +class TestUserAgentHeader: + """Verify the User-Agent header is set on every McpTool.""" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_mcp_tool_has_user_agent_header( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """McpTool.update_headers must be called with User-Agent.""" + expected_ua = "AzureAIFoundry/1.0" + mock_server = _make_mock_server(auth_header="Bearer token") + mock_mcp_tool = _make_mock_mcp_tool() + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[mock_server] + ) + + with ( + patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value=expected_ua), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler", + context=mock_turn_context, + auth_token="token", + ) + + ua_calls = [ + call + for call in mock_mcp_tool.update_headers.call_args_list + if call.args[0] == Constants.Headers.USER_AGENT + ] + assert len(ua_calls) == 1 + assert ua_calls[0].args[1] == expected_ua + + +# --------------------------------------------------------------------------- +# Tests — multiple servers: each gets its own token +# --------------------------------------------------------------------------- + + +class TestMultipleServers: + """Verify each server in the list gets its own per-audience token.""" + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_each_server_gets_its_own_per_audience_token( + self, service, mock_turn_context, mock_auth, mock_project_client + ): + """Two servers with different per-audience tokens must each pass their + own token — not the other server's token or the shared token.""" + token_v1 = "Bearer v1-atg-token" + token_v2 = "Bearer v2-per-audience-token" + + server_v1 = _make_mock_server(auth_header=token_v1) + server_v2 = _make_mock_server(auth_header=token_v2) + server_v2.mcp_server_name = "mcp_AnotherServer" + server_v2.mcp_server_unique_name = "another_server" + + tool_v1 = _make_mock_mcp_tool() + tool_v2 = _make_mock_mcp_tool() + tool_iter = iter([tool_v1, tool_v2]) + + service._mcp_server_configuration_service.list_tool_servers = AsyncMock( + return_value=[server_v1, server_v2] + ) + + with ( + patch(f"{_MODULE}.McpTool", side_effect=lambda **_: next(tool_iter)), + patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), + patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="UA/1.0"), + ): + await service.add_tool_servers_to_agent( + project_client=mock_project_client, + auth=mock_auth, + auth_handler_name="handler", + context=mock_turn_context, + auth_token="shared-discovery-token", + ) + + def _get_auth(tool): + calls = [ + c for c in tool.update_headers.call_args_list + if c.args[0] == Constants.Headers.AUTHORIZATION + ] + return calls[0].args[1] if calls else None + + assert _get_auth(tool_v1) == token_v1 + assert _get_auth(tool_v2) == token_v2 From a135403418846923ed2f2534e70d4f6aaec327f2 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Sun, 12 Apr 2026 13:12:21 +0530 Subject: [PATCH 07/19] 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 --- .../mcp_tool_server_configuration_service.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 0335c8d7..6e880c95 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -135,14 +135,18 @@ async def list_tool_servers( # Determine configuration source based on environment if self._is_development_scenario(): servers = self._load_servers_from_manifest() + # _attach_dev_tokens() already ran inside _load_servers_from_manifest(). + # No OBO exchange in dev: env vars (BEARER_TOKEN_* / BEARER_TOKEN) are the + # auth mechanism, and the gateway is not reachable, so per-audience scopes + # are meaningless here. else: servers = await self._load_servers_from_gateway(agentic_app_id, auth_token, options) - - # Acquire per-audience tokens and attach Authorization headers when auth context provided - if authorization is not None and auth_handler_name is not None and turn_context is not None: - servers = await self._attach_per_audience_tokens( - servers, authorization, auth_handler_name, turn_context - ) + # Prod only: acquire per-audience tokens via OBO for each unique server audience. + # V1 servers share the shared ATG token; V2 servers each get their own audience token. + if authorization is not None and auth_handler_name is not None and turn_context is not None: + servers = await self._attach_per_audience_tokens( + servers, authorization, auth_handler_name, turn_context + ) return servers From 31a6685e782f99f882d4c3b0bf95bcc5e3b39952 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Sun, 12 Apr 2026 15:40:41 +0530 Subject: [PATCH 08/19] Refactor token acquisition to TokenAcquirer strategy pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_ (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 --- .../mcp_tool_server_configuration_service.py | 198 +++++++++++------- .../test_mcp_tool_registration_service.py | 12 +- .../tooling/test_mcp_server_configuration.py | 46 ++-- 3 files changed, 146 insertions(+), 110 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 6e880c95..0b244f83 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -24,7 +24,7 @@ import os import sys from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Awaitable, Callable, Dict, List, Optional from urllib.parse import urlparse # Third-party imports @@ -46,6 +46,17 @@ from microsoft_agents_a365.runtime.utility import Utility as RuntimeUtility +# ============================================================================== +# TYPES +# ============================================================================== + +# Callable that acquires an auth token for a given server and scope. +# Returns the raw token string (without Bearer prefix), or None if unavailable. +# Used by _attach_per_audience_tokens to decouple token acquisition strategy +# (dev env-var reads vs. production OBO exchange) from token attachment logic. +TokenAcquirer = Callable[["MCPServerConfig", str], Awaitable[Optional[str]]] + + # ============================================================================== # CONSTANTS # ============================================================================== @@ -132,22 +143,28 @@ async def list_tool_servers( self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}") - # Determine configuration source based on environment + # Determine configuration source and token acquirer based on environment. if self._is_development_scenario(): servers = self._load_servers_from_manifest() - # _attach_dev_tokens() already ran inside _load_servers_from_manifest(). - # No OBO exchange in dev: env vars (BEARER_TOKEN_* / BEARER_TOKEN) are the - # auth mechanism, and the gateway is not reachable, so per-audience scopes - # are meaningless here. + # Dev: read pre-acquired tokens from env vars (no OBO exchange). + # BEARER_TOKEN_ takes precedence; BEARER_TOKEN is the fallback. + acquire: TokenAcquirer = self._create_dev_token_acquirer() else: servers = await self._load_servers_from_gateway(agentic_app_id, auth_token, options) - # Prod only: acquire per-audience tokens via OBO for each unique server audience. - # V1 servers share the shared ATG token; V2 servers each get their own audience token. - if authorization is not None and auth_handler_name is not None and turn_context is not None: - servers = await self._attach_per_audience_tokens( - servers, authorization, auth_handler_name, turn_context + if ( + authorization is not None + and auth_handler_name is not None + and turn_context is not None + ): + # Prod: acquire per-audience tokens via OBO for each unique server audience. + # V1 servers share the shared ATG token; V2 servers each get their own audience token. + acquire = self._create_obo_token_acquirer( + authorization, auth_handler_name, turn_context ) + else: + return servers + servers = await self._attach_per_audience_tokens(servers, acquire) return servers # -------------------------------------------------------------------------- @@ -167,64 +184,124 @@ def _is_development_scenario(self) -> bool: self._logger.debug(f"Environment: {environment}, Development scenario: {is_dev}") return is_dev - async def _attach_per_audience_tokens( + def _create_dev_token_acquirer(self) -> TokenAcquirer: + """ + Returns a ``TokenAcquirer`` that reads pre-acquired tokens from environment variables. + + The CLI (``a365 develop get-token``) writes tokens to the environment before the agent + starts. Resolution order per server: + + 1. ``BEARER_TOKEN_`` — per-server token (aligns with Node.js) + 2. ``BEARER_TOKEN`` — shared fallback token + + Returns: + TokenAcquirer: Async callable ``(server, scope) → Optional[str]``. + """ + shared_token = os.getenv("BEARER_TOKEN") + + async def acquire(server: MCPServerConfig, _scope: str) -> Optional[str]: + server_name = server.mcp_server_name or "" + per_server_token = os.getenv(f"BEARER_TOKEN_{server_name.upper()}") + token = per_server_token or shared_token + if token: + self._logger.debug( + f"Attached {'per-server' if per_server_token else 'shared'} " + f"dev token for '{server.mcp_server_name}'" + ) + return token or None + + return acquire + + def _create_obo_token_acquirer( self, - servers: List[MCPServerConfig], authorization: Authorization, auth_handler_name: str, turn_context: TurnContext, - ) -> List[MCPServerConfig]: + ) -> TokenAcquirer: """ - Acquire one OAuth token per unique audience and attach an ``Authorization: Bearer`` - header to each server's headers. + Returns a ``TokenAcquirer`` that performs an OBO token exchange per unique scope. - V1 servers (no ``audience`` field, or audience matching the shared ATG AppId) all - share the same ATG-scoped token (one exchange). V2 servers each receive a token - scoped to their own audience GUID. + V1 servers (no ``audience`` field) share the shared ATG-scoped token (one exchange). + V2 servers each receive a token scoped to their own audience GUID. Args: - servers: List of MCP server configs returned from discovery. authorization: Authorization context for token exchange. auth_handler_name: Auth handler name to pass to the token exchange. turn_context: TurnContext to pass to the token exchange. Returns: - List[MCPServerConfig]: New list of server configs with ``Authorization`` headers set. + TokenAcquirer: Async callable ``(server, scope) → str`` (raises on failure). + """ + + async def acquire(server: MCPServerConfig, scope: str) -> Optional[str]: + self._logger.debug( + f"Acquiring OBO token for MCP server '{server.mcp_server_name}' (scope: {scope})" + ) + token_result = await authorization.exchange_token( + turn_context, [scope], auth_handler_name + ) + if token_result is None or not token_result.token: + raise Exception( + f"Failed to obtain token for MCP server '{server.mcp_server_name}'" + f" (scope: {scope})" + ) + return token_result.token + + return acquire + + async def _attach_per_audience_tokens( + self, + servers: List[MCPServerConfig], + acquire: TokenAcquirer, + ) -> List[MCPServerConfig]: + """ + Acquire one token per unique audience scope and attach ``Authorization: Bearer`` headers. + + Caches acquired tokens by scope so each unique audience triggers exactly one + ``acquire`` call regardless of how many servers share that scope. + + V1 servers (no ``audience`` field) all share one token exchange. + V2 servers each receive a token scoped to their own audience GUID. + + Args: + servers: List of MCP server configs returned from discovery. + acquire: ``TokenAcquirer`` callable returned by ``_create_dev_token_acquirer`` or + ``_create_obo_token_acquirer``. Receives ``(server, scope)`` and returns + the raw token string (no Bearer prefix), or ``None`` if unavailable. + + Returns: + List[MCPServerConfig]: New list of server configs with ``Authorization`` headers set + where a token was available. Raises: - Exception: If a token exchange fails for any server. + Exception: If the OBO acquirer fails for any server (propagated from ``acquire``). """ - token_cache: Dict[str, str] = {} # scope → bearer token + token_cache: Dict[str, Optional[str]] = {} # scope → raw token (None = not available) result: List[MCPServerConfig] = [] for server in servers: scope = resolve_token_scope_for_server(server) if scope not in token_cache: - self._logger.debug( - f"Acquiring token for MCP server '{server.mcp_server_name}' (scope: {scope})" - ) - token_result = await authorization.exchange_token( - turn_context, [scope], auth_handler_name + token_cache[scope] = await acquire(server, scope) + + token = token_cache[scope] + if token: + merged_headers: Dict[str, str] = dict(server.headers) if server.headers else {} + merged_headers[Constants.Headers.AUTHORIZATION] = ( + f"{Constants.Headers.BEARER_PREFIX} {token}" ) - if token_result is None or not token_result.token: - raise Exception( - f"Failed to obtain token for MCP server '{server.mcp_server_name}'" - f" (scope: {scope})" - ) - token_cache[scope] = token_result.token - - merged_headers: Dict[str, str] = dict(server.headers) if server.headers else {} - merged_headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {token_cache[scope]}" - ) + final_headers: Optional[Dict[str, str]] = merged_headers + else: + # No token acquired — preserve original headers (including None) unchanged. + final_headers = dict(server.headers) if server.headers else None result.append( MCPServerConfig( mcp_server_name=server.mcp_server_name, mcp_server_unique_name=server.mcp_server_unique_name, url=server.url, - headers=merged_headers, + headers=final_headers, audience=server.audience, scope=server.scope, publisher=server.publisher, @@ -271,7 +348,6 @@ def _load_servers_from_manifest(self) -> List[MCPServerConfig]: if manifest_path and manifest_path.exists(): self._logger.info(f"Loading MCP servers from: {manifest_path}") mcp_servers = self._parse_manifest_file(manifest_path) - self._attach_dev_tokens(mcp_servers) else: self._log_manifest_search_failure() @@ -645,46 +721,6 @@ def _parse_gateway_server_config( # VALIDATION AND UTILITY HELPERS # -------------------------------------------------------------------------- - def _attach_dev_tokens(self, servers: List[MCPServerConfig]) -> None: - """ - Attach per-server Authorization headers from environment variables (local dev only). - - The CLI (``a365 develop get-token``) pre-acquires tokens interactively and writes - them to the environment before the agent starts: - - - ``BEARER_TOKEN_`` — V2 per-server token - - ``BEARER_TOKEN`` — V1 shared ATG token (fallback) - - For each server, the resolution order is: - 1. ``BEARER_TOKEN_`` (per-audience V2 token) - 2. ``BEARER_TOKEN`` (shared V1 ATG token) - - If neither is set, no Authorization header is injected and the server is left as-is. - This method is a no-op in production (``_load_servers_from_manifest`` is never called - there) and when ``authorization`` is provided (``_attach_per_audience_tokens`` takes - precedence via the caller in ``list_tool_servers``). - - Args: - servers: List of MCP server configs parsed from ToolingManifest.json. - """ - shared_token = os.getenv("BEARER_TOKEN") - - for server in servers: - unique_name = server.mcp_server_unique_name or "" - per_server_token = os.getenv(f"BEARER_TOKEN_{unique_name.upper()}") - token = per_server_token or shared_token - - if token: - existing = dict(server.headers) if server.headers else {} - existing[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {token}" - ) - server.headers = existing - self._logger.debug( - f"Attached {'per-server' if per_server_token else 'shared'} " - f"dev token for '{server.mcp_server_unique_name}'" - ) - def _validate_input_parameters(self, agentic_app_id: str, auth_token: str) -> None: """ Validates input parameters for the main API method. diff --git a/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py index 2a17a314..0fe54221 100644 --- a/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py @@ -16,7 +16,6 @@ from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch import pytest - from microsoft_agents_a365.tooling.extensions.azureaifoundry.services import ( McpToolRegistrationService, ) @@ -26,7 +25,9 @@ # Shared helpers # --------------------------------------------------------------------------- -_MODULE = "microsoft_agents_a365.tooling.extensions.azureaifoundry.services.mcp_tool_registration_service" +_MODULE = ( + "microsoft_agents_a365.tooling.extensions.azureaifoundry.services.mcp_tool_registration_service" +) def _make_mock_server(auth_header: str | None = None): @@ -105,7 +106,7 @@ async def test_list_tool_servers_receives_auth_context( """list_tool_servers must receive the full auth context so per-audience token exchange runs for V2 MCP servers.""" auth_token = "discovery-token" - mock_server = _make_mock_server(f"Bearer per-audience-token") + mock_server = _make_mock_server("Bearer per-audience-token") mock_mcp_tool = _make_mock_mcp_tool() service._mcp_server_configuration_service.list_tool_servers = AsyncMock( @@ -141,7 +142,7 @@ async def test_list_tool_servers_called_with_correct_keyword_args( ): """Verify keyword argument names match list_tool_servers signature exactly.""" auth_token = "token-xyz" - mock_server = _make_mock_server(f"Bearer v2-token") + mock_server = _make_mock_server("Bearer v2-token") mock_mcp_tool = _make_mock_mcp_tool() list_tool_servers_mock = AsyncMock(return_value=[mock_server]) @@ -377,7 +378,8 @@ async def test_each_server_gets_its_own_per_audience_token( def _get_auth(tool): calls = [ - c for c in tool.update_headers.call_args_list + c + for c in tool.update_headers.call_args_list if c.args[0] == Constants.Headers.AUTHORIZATION ] return calls[0].args[1] if calls else None diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index bb769ae2..928907e3 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -344,7 +344,12 @@ def test_v2_api_uri_audience_null_scope_falls_back_to_default(self): class TestAttachPerAudienceTokens: - """Tests for McpToolServerConfigurationService._attach_per_audience_tokens().""" + """Tests for McpToolServerConfigurationService._attach_per_audience_tokens(). + + Since _attach_per_audience_tokens now accepts a TokenAcquirer callable, + these tests use service._create_obo_token_acquirer() to build the acquirer + from a mock authorization object — matching real production usage. + """ ATG_APP_ID = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" @@ -373,10 +378,9 @@ async def test_v1_server_gets_atg_token(self, service): """V1 server (no audience) receives ATG-scoped token.""" servers = [self._make_server("mail")] authorization, turn_context = self._make_auth_context("atg-token") + acquire = service._create_obo_token_acquirer(authorization, "handler", turn_context) - result = await service._attach_per_audience_tokens( - servers, authorization, "handler", turn_context - ) + result = await service._attach_per_audience_tokens(servers, acquire) assert len(result) == 1 assert result[0].headers["Authorization"] == "Bearer atg-token" @@ -390,10 +394,9 @@ async def test_v2_server_gets_per_audience_token(self, service): guid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" servers = [self._make_server("calendar", audience=guid)] authorization, turn_context = self._make_auth_context("v2-token") + acquire = service._create_obo_token_acquirer(authorization, "handler", turn_context) - result = await service._attach_per_audience_tokens( - servers, authorization, "handler", turn_context - ) + result = await service._attach_per_audience_tokens(servers, acquire) assert result[0].headers["Authorization"] == "Bearer v2-token" authorization.exchange_token.assert_called_once_with( @@ -409,10 +412,9 @@ async def test_multiple_v1_servers_share_one_token_exchange(self, service): self._make_server("files"), ] authorization, turn_context = self._make_auth_context("shared-atg-token") + acquire = service._create_obo_token_acquirer(authorization, "handler", turn_context) - result = await service._attach_per_audience_tokens( - servers, authorization, "handler", turn_context - ) + result = await service._attach_per_audience_tokens(servers, acquire) assert all(s.headers["Authorization"] == "Bearer shared-atg-token" for s in result) authorization.exchange_token.assert_called_once() @@ -441,10 +443,9 @@ async def fake_exchange(ctx, scopes, handler): authorization.exchange_token = fake_exchange turn_context = MagicMock() + acquire = service._create_obo_token_acquirer(authorization, "handler", turn_context) - result = await service._attach_per_audience_tokens( - servers, authorization, "handler", turn_context - ) + result = await service._attach_per_audience_tokens(servers, acquire) assert call_count[0] == 3 # ATG + guid1 + guid2 assert result[0].headers["Authorization"] == f"Bearer token-for-{self.ATG_APP_ID}/.default" @@ -455,29 +456,27 @@ async def fake_exchange(ctx, scopes, handler): @pytest.mark.asyncio async def test_raises_when_token_exchange_returns_none(self, service): - """Exception raised when token exchange returns None.""" + """Exception raised when OBO token exchange returns None.""" servers = [self._make_server("mail")] authorization = MagicMock() authorization.exchange_token = AsyncMock(return_value=None) + acquire = service._create_obo_token_acquirer(authorization, "handler", MagicMock()) with pytest.raises(Exception, match="Failed to obtain token"): - await service._attach_per_audience_tokens( - servers, authorization, "handler", MagicMock() - ) + await service._attach_per_audience_tokens(servers, acquire) @pytest.mark.asyncio async def test_raises_when_token_is_empty(self, service): - """Exception raised when token result has empty token string.""" + """Exception raised when OBO token result has an empty token string.""" servers = [self._make_server("mail")] authorization = MagicMock() token_result = MagicMock() token_result.token = "" authorization.exchange_token = AsyncMock(return_value=token_result) + acquire = service._create_obo_token_acquirer(authorization, "handler", MagicMock()) with pytest.raises(Exception, match="Failed to obtain token"): - await service._attach_per_audience_tokens( - servers, authorization, "handler", MagicMock() - ) + await service._attach_per_audience_tokens(servers, acquire) @pytest.mark.asyncio async def test_preserves_existing_server_headers(self, service): @@ -489,10 +488,9 @@ async def test_preserves_existing_server_headers(self, service): headers={"X-Custom": "my-value"}, ) authorization, turn_context = self._make_auth_context("tok") + acquire = service._create_obo_token_acquirer(authorization, "handler", turn_context) - result = await service._attach_per_audience_tokens( - [server], authorization, "handler", turn_context - ) + result = await service._attach_per_audience_tokens([server], acquire) assert result[0].headers["X-Custom"] == "my-value" assert result[0].headers["Authorization"] == "Bearer tok" From 8cc5e32c29a098643ac4e4e5676aa1a812f5a00d Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Tue, 14 Apr 2026 16:41:56 +0530 Subject: [PATCH 09/19] updated prod endpoint --- .../microsoft_agents_a365/tooling/utils/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index 9de4e8f1..813fbce7 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -15,7 +15,7 @@ # Constants for base URLs -MCP_PLATFORM_PROD_BASE_URL = "https://test.agent365.svc.cloud.dev.microsoft" +MCP_PLATFORM_PROD_BASE_URL = "https://agent365.svc.cloud.microsoft" # API endpoint paths CHAT_HISTORY_ENDPOINT_PATH = "/agents/real-time-threat-protection/chat-message" From 77fc5d8453cec923eac1bc0772cf61135705bf2f Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Wed, 15 Apr 2026 17:43:41 +0530 Subject: [PATCH 10/19] 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. --- .../mcp_tool_server_configuration_service.py | 49 +++++++++++++++++-- .../tooling/utils/constants.py | 3 ++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 0b244f83..7a28a13c 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -23,6 +23,7 @@ import logging import os import sys +import uuid from pathlib import Path from typing import Any, Awaitable, Callable, Dict, List, Optional from urllib.parse import urlparse @@ -150,7 +151,9 @@ async def list_tool_servers( # BEARER_TOKEN_ takes precedence; BEARER_TOKEN is the fallback. acquire: TokenAcquirer = self._create_dev_token_acquirer() else: - servers = await self._load_servers_from_gateway(agentic_app_id, auth_token, options) + servers = await self._load_servers_from_gateway( + agentic_app_id, auth_token, options, turn_context + ) if ( authorization is not None and auth_handler_name is not None @@ -475,7 +478,11 @@ def _log_manifest_search_failure(self) -> None: # -------------------------------------------------------------------------- async def _load_servers_from_gateway( - self, agentic_app_id: str, auth_token: str, options: ToolOptions + self, + agentic_app_id: str, + auth_token: str, + options: ToolOptions, + turn_context: Optional[TurnContext] = None, ) -> List[MCPServerConfig]: """ Reads MCP server configurations from tooling gateway endpoint for production scenario. @@ -484,6 +491,8 @@ async def _load_servers_from_gateway( agentic_app_id: Agentic App ID for the agent. auth_token: Authentication token to access the tooling gateway. options: ToolOptions instance containing optional parameters. + turn_context: Optional TurnContext used to derive the correlation ID from + ``activity.id``. A new UUID is generated when not provided. Returns: List[MCPServerConfig]: List of MCP server configurations from tooling gateway. @@ -495,7 +504,7 @@ async def _load_servers_from_gateway( try: config_endpoint = get_tooling_gateway_for_digital_worker(agentic_app_id) - headers = self._prepare_gateway_headers(auth_token, options) + headers = self._prepare_gateway_headers(auth_token, options, turn_context) self._logger.info(f"Calling tooling gateway endpoint: {config_endpoint}") @@ -533,7 +542,8 @@ def _prepare_gateway_headers( Args: auth_token: Authentication token. options: ToolOptions instance containing optional parameters. - turn_context: Optional TurnContext for extracting agent blueprint ID for request headers. + turn_context: Optional TurnContext for extracting agent blueprint ID and + correlation ID from ``activity.id``. Returns: Dictionary of HTTP headers. @@ -550,8 +560,39 @@ def _prepare_gateway_headers( if agent_id: headers[Constants.Headers.AGENT_ID] = agent_id + # Add x-ms-correlation-id: prefer activity.id from TurnContext, fall back to a new UUID + correlation_id = self._resolve_correlation_id(turn_context) + headers[Constants.Headers.CORRELATION_ID] = correlation_id + self._logger.debug(f"Gateway request correlation ID: {correlation_id}") + return headers + def _resolve_correlation_id(self, turn_context: Optional[TurnContext] = None) -> str: + """ + Resolves the correlation ID to attach to outbound gateway requests. + + Uses ``turn_context.activity.id`` when available so the gateway log entry can be + correlated with the inbound activity. Falls back to a newly generated UUID4 when + no context is provided. + + Args: + turn_context: Optional TurnContext to extract the activity ID from. + + Returns: + str: Correlation ID string (non-empty). + """ + try: + if ( + turn_context is not None + and turn_context.activity is not None + and turn_context.activity.id + ): + return turn_context.activity.id + except (AttributeError, TypeError): + pass + + return str(uuid.uuid4()) + def _resolve_agent_id_for_header( self, auth_token: str, turn_context: Optional[TurnContext] = None ) -> Optional[str]: diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/constants.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/constants.py index d1ff406c..c978a448 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/constants.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/constants.py @@ -33,3 +33,6 @@ class Headers: #: Header name for the subchannel ID. SUBCHANNEL_ID = "x-ms-subchannel-id" + + #: Header name for the correlation ID used to trace a request across services. + CORRELATION_ID = "x-ms-correlation-id" From cac289297968c2385bf82231aa85ac717e29d817 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 16:16:49 +0530 Subject: [PATCH 11/19] 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_ -> BEARER_TOKEN_) - 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 --- .../services/mcp_tool_registration_service.py | 20 +- .../services/mcp_tool_registration_service.py | 15 +- .../openai/mcp_tool_registration_service.py | 19 +- .../services/mcp_tool_registration_service.py | 43 +- .../CHANGELOG.md | 10 +- .../mcp_tool_server_configuration_service.py | 390 +++++++++--------- .../tooling/utils/utility.py | 57 ++- .../test_mcp_tool_registration_service.py | 4 +- .../tooling/test_mcp_server_configuration.py | 54 ++- 9 files changed, 357 insertions(+), 255 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py index c02bcb16..2d2ff42c 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py @@ -4,7 +4,7 @@ import logging import uuid from datetime import datetime, timezone -from typing import Any, List, Optional, Sequence, Union +from typing import List, Optional, Sequence, Union from agent_framework import RawAgent, Message, BaseHistoryProvider, MCPStreamableHTTPTool from agent_framework.azure import AzureOpenAIChatClient @@ -22,6 +22,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, ) @@ -57,7 +58,7 @@ async def add_tool_servers_to_agent( self, chat_client: Union[OpenAIChatClient, AzureOpenAIChatClient], agent_instructions: str, - initial_tools: List[Any], + initial_tools: List[object], auth: Authorization, auth_handler_name: str, turn_context: TurnContext, @@ -82,13 +83,15 @@ 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}") @@ -124,6 +127,13 @@ async def add_tool_servers_to_agent( ) } 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] = ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + ) headers = {**base_headers, **server_headers} # server auth takes precedence # Create httpx client with auth headers configured diff --git a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py index 10207012..60ee5c78 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py @@ -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, ) @@ -76,12 +77,15 @@ 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) @@ -124,6 +128,13 @@ async def add_tool_servers_to_agent( ) } 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] = ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + ) headers = {**base_headers, **server_headers} # per-audience token takes precedence server_info = McpToolset( diff --git a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py index 4d5a751e..20db917f 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py @@ -32,6 +32,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, ) @@ -88,17 +89,16 @@ async def add_tool_servers_to_agent( New Agent instance with all MCP servers, or original agent if no new servers """ - if auth_token is None or auth_token.strip() == "": + is_dev = is_development_environment() + if (auth_token is None or auth_token.strip() == "") 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(context, scopes, auth_handler_name) auth_token = authToken.token - # Get MCP server configurations from the configuration service - # mcp_server_configs = [] - # TODO: radevika: Update once the common project is merged. - options = ToolOptions(orchestrator_name=self._orchestrator_name) - 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}") mcp_server_configs = await self.config_service.list_tool_servers( agentic_app_id=agentic_app_id, @@ -161,6 +161,13 @@ async def add_tool_servers_to_agent( ) } server_headers = dict(si.headers) if si.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] = ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + ) headers = { **base_headers, **server_headers, diff --git a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py index b908720c..6d67a7ca 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py @@ -15,7 +15,7 @@ import re import uuid from datetime import datetime, timezone -from typing import Any, List, Optional, Sequence +from typing import List, Optional, Sequence # Third-party imports from semantic_kernel import kernel as sk @@ -33,6 +33,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, ) @@ -74,11 +75,11 @@ def __init__( ) if self._strict_parameter_validation: self._logger.info( - "🔒 Strict parameter validation enabled - only schema-defined parameters are allowed" + "Strict parameter validation enabled - only schema-defined parameters are allowed" ) else: self._logger.info( - "🔓 Strict parameter validation disabled - dynamic parameters are allowed" + "Strict parameter validation disabled - dynamic parameters are allowed" ) # ============================================================================ @@ -108,12 +109,15 @@ async def add_tool_servers_to_agent( Exception: If there's an error connecting to or configuring MCP servers. """ - 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(context, scopes, auth_handler_name) auth_token = authToken.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._validate_inputs(kernel, agentic_app_id, auth_token) # Get and process servers @@ -126,7 +130,7 @@ async def add_tool_servers_to_agent( auth_handler_name=auth_handler_name, turn_context=context, ) - self._logger.info(f"🔧 Adding MCP tools from {len(servers)} servers") + self._logger.info(f"Adding MCP tools from {len(servers)} servers") # Process each server (matching C# foreach pattern) for server in servers: @@ -137,6 +141,13 @@ async def add_tool_servers_to_agent( ) } server_headers = dict(server.headers) if server.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] = ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + ) headers = {**base_headers, **server_headers} # per-audience token takes precedence # Use the URL from server (always populated by the configuration service) @@ -166,22 +177,26 @@ async def add_tool_servers_to_agent( self._connected_plugins.append(plugin) self._logger.info( - f"✅ Connected and added MCP plugin for: {server.mcp_server_name}" + f"Connected and added MCP plugin for: {server.mcp_server_name}" ) except Exception as e: self._logger.error(f"Failed to add tools from {server.mcp_server_name}: {str(e)}") - self._logger.info("✅ Successfully configured MCP tool servers for the agent!") + self._logger.info("Successfully configured MCP tool servers for the agent") # ============================================================================ # Private Methods - Input Validation & Processing # ============================================================================ - def _validate_inputs(self, kernel: Any, agentic_app_id: str, auth_token: str) -> None: - """Validate all required inputs.""" + def _validate_inputs( + self, kernel: sk.Kernel, agentic_app_id: str, auth_token: Optional[str] + ) -> None: + """Validate all required inputs. In dev mode only kernel is checked.""" if kernel is None: raise ValueError("kernel cannot be None") + if is_development_environment(): + return if not agentic_app_id or not agentic_app_id.strip(): raise ValueError("agentic_app_id cannot be null or empty") if not auth_token or not auth_token.strip(): @@ -550,7 +565,7 @@ def _extract_or_generate_timestamp( async def cleanup_connections(self) -> None: """Clean up all connected MCP plugins.""" - self._logger.info(f"🧹 Cleaning up {len(self._connected_plugins)} MCP plugin connections") + self._logger.info(f"Cleaning up {len(self._connected_plugins)} MCP plugin connections") for plugin in self._connected_plugins: try: @@ -559,10 +574,10 @@ async def cleanup_connections(self) -> None: elif hasattr(plugin, "disconnect"): await plugin.disconnect() self._logger.debug( - f"✅ Closed connection for plugin: {getattr(plugin, 'name', 'unknown')}" + f"Closed connection for plugin: {getattr(plugin, 'name', 'unknown')}" ) except Exception as e: - self._logger.warning(f"⚠️ Error closing plugin connection: {e}") + self._logger.warning(f"Error closing plugin connection: {e}") self._connected_plugins.clear() - self._logger.info("✅ All MCP plugin connections cleaned up") + self._logger.info("All MCP plugin connections cleaned up") diff --git a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md index 3ed97c6e..73e4267c 100644 --- a/libraries/microsoft-agents-a365-tooling/CHANGELOG.md +++ b/libraries/microsoft-agents-a365-tooling/CHANGELOG.md @@ -11,25 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added MCP V1/V2 per-audience token acquisition support in `McpToolServerConfigurationService.list_tool_servers()`. When `authorization`, `auth_handler_name`, and `turn_context` are provided, each MCP server receives its own OAuth token scoped to its audience — V1 servers (no audience, or shared ATG AppId) share a single ATG-scoped token; V2 servers (unique non-ATG audience GUID or `api://` URI) each receive a token scoped to `{audience}/{scope}` (or `{audience}/.default` when scope is absent and pre-consented) - Added `_attach_per_audience_tokens()` private method to `McpToolServerConfigurationService` — acquires one token per unique scope, caches within the call to avoid redundant exchanges, and attaches `Authorization: Bearer` headers to each server config +- Added `_create_dev_token_acquirer()` private method to `McpToolServerConfigurationService` — returns a `TokenAcquirer` closure that reads pre-acquired tokens from environment variables written by the `a365 develop get-token` CLI. Resolution order per server: (1) `BEARER_TOKEN_` (keyed on `mcp_server_name`, uppercased), then (2) `BEARER_TOKEN` shared fallback. Any existing `Bearer ` prefix (any casing) is stripped before the token is returned so the `Authorization` header is never doubled +- Added `_create_obo_token_acquirer()` private method to `McpToolServerConfigurationService` — returns a `TokenAcquirer` closure that performs an OBO token exchange via `Authorization.exchange_token()` for production use; one exchange per unique audience scope - Added `resolve_token_scope_for_server()` utility function to derive the correct OAuth scope for a given `MCPServerConfig` based on its `audience` and `scope` fields - Added `audience`, `scope`, `publisher`, and `headers` fields to `MCPServerConfig` - Gateway discovery endpoint bumped to `/agents/v2/{id}/mcpServers` -- `_parse_gateway_server_config()` and `_parse_manifest_server_config()` now map `audience`, `scope`, and `publisher` fields from gateway/manifest responses into `MCPServerConfig` - -- Added `_attach_dev_tokens()` private method to `McpToolServerConfigurationService` — reads `BEARER_TOKEN_` and `BEARER_TOKEN` environment variables written by the `a365 develop get-token` CLI and attaches per-server `Authorization: Bearer` headers during local dev manifest loading; no-op in production +- `_parse_gateway_server_config()` and `_parse_manifest_server_config()` merged into a single `_parse_server_config()` method — both gateway and manifest payloads share the same JSON field schema; the unified method maps `audience`, `scope`, and `publisher` fields from either source into `MCPServerConfig` ### Changed - OpenAI, Semantic Kernel, and Google ADK extensions now pass auth context to `list_tool_servers()` and merge per-server headers (`{**base_headers, **server.headers}`) instead of injecting a single shared ATG token for all servers — fully backward compatible, V1 agents continue to receive the same shared ATG token - `_extract_server_unique_name()` now falls back to `mcpServerName` when `mcpServerUniqueName` is absent from the manifest or gateway response -- `_parse_manifest_server_config()` and `_parse_gateway_server_config()` now normalize `"null"` scope strings and `"default"` audience strings to `None` to prevent incorrect V2 token scope resolution +- `_parse_server_config()` (the unified replacement for the former `_parse_manifest_server_config()` / `_parse_gateway_server_config()`) now normalizes `"null"` scope strings and `"default"` audience strings to `None` to prevent incorrect V2 token scope resolution - `resolve_token_scope_for_server()` now treats `"default"` audience as V1 (shared ATG token) as a defense-in-depth guard ### Notes - **Backward compatible**: agents with V1 manifests (null audience or shared ATG AppId) work identically with the new SDK — no token exchange behaviour changes - **Migration required for V2**: agents upgraded to V2 blueprint permissions (per-audience MCP servers) require this SDK version. Running a V2 blueprint with the old SDK will result in MCP tool auth failures (401/403) -- **Local dev token flow**: run `a365 develop get-token` before starting the agent locally; the CLI writes `BEARER_TOKEN` (V1 shared) and `BEARER_TOKEN_` (V2 per-server) to the environment, which the SDK reads automatically from the manifest path +- **Local dev token flow**: run `a365 develop get-token` before starting the agent locally; the CLI writes `BEARER_TOKEN` (shared fallback) and `BEARER_TOKEN_` (per-server, keyed on the server's `mcpServerName` value uppercased) to the environment, which the SDK reads automatically during manifest-based discovery - Added `send_chat_history` method to `McpToolServerConfigurationService` for sending chat conversation history to the MCP platform for real-time threat protection analysis - Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 7a28a13c..66037dbe 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -22,10 +22,10 @@ import json import logging import os -import sys import uuid +from dataclasses import replace as dataclass_replace from pathlib import Path -from typing import Any, Awaitable, Callable, Dict, List, Optional +from typing import Awaitable, Callable, Dict, List, Optional from urllib.parse import urlparse # Third-party imports @@ -36,9 +36,12 @@ from ..models import ChatHistoryMessage, ChatMessageRequest, MCPServerConfig, ToolOptions from ..utils import Constants from ..utils.utility import ( + ATG_APP_ID, + ATG_APP_ID_URI, build_mcp_server_url, get_chat_history_endpoint, get_tooling_gateway_for_digital_worker, + is_development_environment, resolve_token_scope_for_server, ) @@ -104,7 +107,7 @@ def __init__(self, logger: Optional[logging.Logger] = None): async def list_tool_servers( self, agentic_app_id: str, - auth_token: str, + auth_token: Optional[str] = None, options: Optional[ToolOptions] = None, authorization: Optional[Authorization] = None, auth_handler_name: Optional[str] = None, @@ -165,6 +168,20 @@ async def list_tool_servers( authorization, auth_handler_name, turn_context ) else: + # Legacy call without auth context — guard against V2 servers. + # V2 servers require per-audience OBO exchange; returning them without a + # token would cause silent 401s downstream. Raise early with a clear message + # so callers get an actionable migration hint (mirrors Node.js behaviour). + v2_servers = [s for s in servers if self._is_v2_server(s)] + if v2_servers: + names = ", ".join( + s.mcp_server_name or s.mcp_server_unique_name for s in v2_servers + ) + raise Exception( + f"MCP servers [{names}] require per-audience token exchange (V2) but " + "no authorization context was provided. Pass authorization, " + "auth_handler_name, and turn_context to list_tool_servers()." + ) return servers servers = await self._attach_per_audience_tokens(servers, acquire) @@ -178,15 +195,34 @@ def _is_development_scenario(self) -> bool: """ Determines if this is a development scenario. + Delegates to ``is_development_environment()`` from utility so all callers + use the same env-var resolution order. + Returns: bool: True if running in development mode, False otherwise. """ - environment = os.getenv("ENVIRONMENT", "Development") - - is_dev = environment.lower() == "development" - self._logger.debug(f"Environment: {environment}, Development scenario: {is_dev}") + is_dev = is_development_environment() + self._logger.debug(f"Development scenario: {is_dev}") return is_dev + def _is_v2_server(self, server: MCPServerConfig) -> bool: + """ + Returns True if the server requires a per-audience token (V2). + + V2 servers carry a distinct ``audience`` that is neither the shared ATG AppId + (bare GUID or ``api://`` URI form) nor the sentinel value ``"default"``. + Uses the same normalization as ``resolve_token_scope_for_server`` so the + V1/V2 classification is always consistent. + """ + if server.audience is None: + return False + audience = server.audience.strip().lower() + return ( + audience != "default" + and audience != ATG_APP_ID + and audience != ATG_APP_ID_URI + ) + def _create_dev_token_acquirer(self) -> TokenAcquirer: """ Returns a ``TokenAcquirer`` that reads pre-acquired tokens from environment variables. @@ -197,6 +233,10 @@ def _create_dev_token_acquirer(self) -> TokenAcquirer: 1. ``BEARER_TOKEN_`` — per-server token (aligns with Node.js) 2. ``BEARER_TOKEN`` — shared fallback token + Tokens are returned **without** a ``Bearer `` prefix. If the env var already contains + a ``Bearer `` prefix (any casing), it is stripped so that + ``_attach_per_audience_tokens`` does not produce ``Authorization: Bearer Bearer …``. + Returns: TokenAcquirer: Async callable ``(server, scope) → Optional[str]``. """ @@ -206,12 +246,17 @@ async def acquire(server: MCPServerConfig, _scope: str) -> Optional[str]: server_name = server.mcp_server_name or "" per_server_token = os.getenv(f"BEARER_TOKEN_{server_name.upper()}") token = per_server_token or shared_token - if token: - self._logger.debug( - f"Attached {'per-server' if per_server_token else 'shared'} " - f"dev token for '{server.mcp_server_name}'" - ) - return token or None + if not token: + return None + # Strip an existing "Bearer " prefix (case-insensitive) so the caller + # always receives a raw token and the Authorization header is never doubled. + if token.lower().startswith("bearer "): + token = token[7:] + self._logger.debug( + f"Attached {'per-server' if per_server_token else 'shared'} " + f"dev token for '{server.mcp_server_name}'" + ) + return token return acquire @@ -299,17 +344,7 @@ async def _attach_per_audience_tokens( # No token acquired — preserve original headers (including None) unchanged. final_headers = dict(server.headers) if server.headers else None - result.append( - MCPServerConfig( - mcp_server_name=server.mcp_server_name, - mcp_server_unique_name=server.mcp_server_unique_name, - url=server.url, - headers=final_headers, - audience=server.audience, - scope=server.scope, - publisher=server.publisher, - ) - ) + result.append(dataclass_replace(server, headers=final_headers)) return result @@ -343,77 +378,62 @@ def _load_servers_from_manifest(self) -> List[MCPServerConfig]: Raises: Exception: If manifest file cannot be read or parsed. """ - mcp_servers: List[MCPServerConfig] = [] - try: - manifest_path = self._find_manifest_file() + search_locations = self._get_manifest_search_locations() + manifest_path = self._find_manifest_file(search_locations) - if manifest_path and manifest_path.exists(): + if manifest_path is not None: self._logger.info(f"Loading MCP servers from: {manifest_path}") - mcp_servers = self._parse_manifest_file(manifest_path) - else: - self._log_manifest_search_failure() + return self._parse_manifest_file(manifest_path) + + self._logger.info( + f"ToolingManifest.json not found. Checked {len(search_locations)} locations" + ) + for path in search_locations: + self._logger.debug(f" Checked: {path}") + self._logger.info( + "Please ensure ToolingManifest.json exists in your project's output directory." + ) + return [] except Exception as e: raise Exception( f"Failed to read MCP servers from ToolingManifest.json: {str(e)}" ) from e - return mcp_servers - - def _find_manifest_file(self) -> Optional[Path]: + def _find_manifest_file(self, search_locations: List[Path]) -> Optional[Path]: """ - Searches for ToolingManifest.json in various common locations. + Searches for ToolingManifest.json in the provided locations. + + Args: + search_locations: Ordered list of paths to check. Returns: Path to manifest file if found, None otherwise. """ - search_locations = self._get_manifest_search_locations() - for potential_path in search_locations: self._logger.debug(f"Checking for manifest at: {potential_path}") if potential_path.exists(): self._logger.info(f"Found manifest at: {potential_path}") return potential_path - else: - self._logger.debug(f"Manifest not found at: {potential_path}") return None def _get_manifest_search_locations(self) -> List[Path]: """ - Gets list of potential locations for ToolingManifest.json. + Gets the ordered list of candidate paths for ToolingManifest.json. + + Searches the current working directory and its parent only. File-relative + path traversal is not used because it is unreliable for installed packages. Returns: List of Path objects to search for the manifest file. """ current_dir = Path.cwd() - search_locations = [] - - # Current working directory - search_locations.append(current_dir / "ToolingManifest.json") - - # Parent directory - search_locations.append(current_dir.parent / "ToolingManifest.json") - - # Script location and project root - if __file__: - if hasattr(sys, "_MEIPASS"): - # Running as PyInstaller bundle - base_dir = Path(sys._MEIPASS) - else: - # Running as normal Python script - current_file_path = Path(__file__) - # Navigate to project root - base_dir = current_file_path.parent.parent.parent.parent - - search_locations.extend( - [ - base_dir / "ToolingManifest.json", - ] - ) - - return search_locations + return [ + current_dir / "ToolingManifest.json", + current_dir.parent / "ToolingManifest.json", + ] def _parse_manifest_file(self, manifest_path: Path) -> List[MCPServerConfig]: """ @@ -425,53 +445,29 @@ def _parse_manifest_file(self, manifest_path: Path) -> List[MCPServerConfig]: Returns: List of parsed MCP server configurations. """ - mcp_servers: List[MCPServerConfig] = [] - with open(manifest_path, "r", encoding="utf-8") as file: - json_content = file.read() - - print(f"📄 Manifest content: {json_content}") - manifest_data = json.loads(json_content) - - if "mcpServers" in manifest_data: - print("✅ Found 'mcpServers' section in ToolingManifest.json") - self._logger.info("Found 'mcpServers' section in ToolingManifest.json") - mcp_servers_data = manifest_data["mcpServers"] - - if isinstance(mcp_servers_data, list): - print(f"📊 Processing {len(mcp_servers_data)} server entries") - for server_element in mcp_servers_data: - print(f"🔧 Processing server element: {server_element}") - server_config = self._parse_manifest_server_config(server_element) - if server_config is not None: - print( - f"✅ Created server config: {server_config.mcp_server_name} -> {server_config.mcp_server_unique_name}" - ) - mcp_servers.append(server_config) - else: - print(f"❌ Failed to parse server config from: {server_element}") - else: - print("❌ No 'mcpServers' section found in ToolingManifest.json") + manifest_data = json.load(file) - print(f"📊 Final result: Loaded {len(mcp_servers)} MCP server configurations") - self._logger.info(f"Loaded {len(mcp_servers)} MCP server configurations") + if "mcpServers" not in manifest_data: + self._logger.warning("No 'mcpServers' section found in ToolingManifest.json") + return [] - return mcp_servers + self._logger.info("Found 'mcpServers' section in ToolingManifest.json") + mcp_servers_data = manifest_data["mcpServers"] - def _log_manifest_search_failure(self) -> None: - """Logs information about failed manifest file search.""" - search_locations = self._get_manifest_search_locations() + if not isinstance(mcp_servers_data, list): + self._logger.warning("'mcpServers' in ToolingManifest.json is not a list — skipping") + return [] - print("❌ ToolingManifest.json not found. Checked locations:") - for path in search_locations: - print(f" - {path}") + self._logger.debug(f"Processing {len(mcp_servers_data)} server entries from manifest") + mcp_servers: List[MCPServerConfig] = [] + for server_element in mcp_servers_data: + server_config = self._parse_server_config(server_element) + if server_config is not None: + mcp_servers.append(server_config) - self._logger.info( - f"ToolingManifest.json not found. Checked {len(search_locations)} locations" - ) - self._logger.info( - "Please ensure ToolingManifest.json exists in your project's output directory." - ) + self._logger.info(f"Loaded {len(mcp_servers)} MCP server configurations from manifest") + return mcp_servers # -------------------------------------------------------------------------- # PRODUCTION: GATEWAY-BASED CONFIGURATION @@ -500,21 +496,21 @@ async def _load_servers_from_gateway( Raises: Exception: If there's an error communicating with the tooling gateway. """ - mcp_servers: List[MCPServerConfig] = [] - try: config_endpoint = get_tooling_gateway_for_digital_worker(agentic_app_id) headers = self._prepare_gateway_headers(auth_token, options, turn_context) self._logger.info(f"Calling tooling gateway endpoint: {config_endpoint}") - async with aiohttp.ClientSession() as session: + timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT_SECONDS) + async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(config_endpoint, headers=headers) as response: if response.status == 200: mcp_servers = await self._parse_gateway_response(response) self._logger.info( f"Retrieved {len(mcp_servers)} MCP tool servers from tooling gateway" ) + return mcp_servers else: raise Exception(f"HTTP {response.status}: {await response.text()}") @@ -531,8 +527,6 @@ async def _load_servers_from_gateway( self._logger.error(error_msg) raise Exception(error_msg) from e - return mcp_servers - def _prepare_gateway_headers( self, auth_token: str, options: ToolOptions, turn_context: Optional[TurnContext] = None ) -> Dict[str, str]: @@ -639,20 +633,37 @@ async def _parse_gateway_response( """ Parses the response from the tooling gateway. + Supports two response shapes: + - Wrapped: ``{"mcpServers": [...]}`` + - Raw array: ``[...]`` (legacy V1 gateway format) + Args: response: HTTP response from the gateway. Returns: List of parsed MCP server configurations. """ - mcp_servers: List[MCPServerConfig] = [] - - response_text = await response.text() - config_data = json.loads(response_text) + config_data = await response.json(content_type=None) + + server_elements: Optional[List[object]] = None + if isinstance(config_data, list): + # Raw array format (legacy V1 gateway returns bare array) + self._logger.debug("Gateway returned raw array response") + server_elements = config_data + elif isinstance(config_data, dict) and isinstance(config_data.get("mcpServers"), list): + # Wrapped format: {"mcpServers": [...]} + self._logger.debug("Gateway returned wrapped mcpServers response") + server_elements = config_data["mcpServers"] + else: + self._logger.warning( + 'Unexpected gateway response format: expected a list or {"mcpServers": [...]}' + ) + return [] - if "mcpServers" in config_data and isinstance(config_data["mcpServers"], list): - for server_element in config_data["mcpServers"]: - server_config = self._parse_gateway_server_config(server_element) + mcp_servers: List[MCPServerConfig] = [] + for server_element in server_elements: + if isinstance(server_element, dict): + server_config = self._parse_server_config(server_element) if server_config is not None: mcp_servers.append(server_config) @@ -662,65 +673,20 @@ async def _parse_gateway_response( # CONFIGURATION PARSING HELPERS # -------------------------------------------------------------------------- - def _parse_manifest_server_config( - self, server_element: Dict[str, Any] + def _parse_server_config( + self, server_element: Dict[str, object] ) -> Optional[MCPServerConfig]: """ - Parses a server configuration from manifest data, constructing full URL. - - Args: - server_element: Dictionary containing server configuration from manifest. - - Returns: - MCPServerConfig object or None if parsing fails. - """ - try: - mcp_server_name = self._extract_server_name(server_element) - mcp_server_unique_name = self._extract_server_unique_name(server_element) - - if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name): - return None - - # Check if a URL is provided - endpoint = self._extract_server_url(server_element) - - # Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction - server_name = mcp_server_name or mcp_server_unique_name - - # Determine the final URL: use custom URL if provided, otherwise construct it - final_url = endpoint if endpoint else build_mcp_server_url(server_name) - - scope_raw = server_element.get("scope") - scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw - - audience_raw = server_element.get("audience") - audience = ( - None if not audience_raw or audience_raw.lower() == "default" else audience_raw - ) + Parses a server configuration from manifest or gateway response data. - return MCPServerConfig( - mcp_server_name=mcp_server_name, - mcp_server_unique_name=mcp_server_unique_name, - url=final_url, - audience=audience, - scope=scope, - publisher=server_element.get("publisher"), - ) - - except Exception: - return None - - def _parse_gateway_server_config( - self, server_element: Dict[str, Any] - ) -> Optional[MCPServerConfig]: - """ - Parses a server configuration from gateway response data. + Handles both development (manifest) and production (gateway) payloads — + the two sources share the same JSON field schema. Args: - server_element: Dictionary containing server configuration from gateway. + server_element: Dictionary containing server configuration. Returns: - MCPServerConfig object or None if parsing fails. + MCPServerConfig object, or None if the element is invalid or unparseable. """ try: mcp_server_name = self._extract_server_name(server_element) @@ -729,56 +695,72 @@ def _parse_gateway_server_config( if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name): return None - # Check if a URL is provided by the gateway endpoint = self._extract_server_url(server_element) - - # Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction + # Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name server_name = mcp_server_name or mcp_server_unique_name - - # Determine the final URL: use custom URL if provided, otherwise construct it final_url = endpoint if endpoint else build_mcp_server_url(server_name) scope_raw = server_element.get("scope") - scope = None if not scope_raw or scope_raw.lower() == "null" else scope_raw + scope = ( + None + if not scope_raw + or (isinstance(scope_raw, str) and scope_raw.lower() == "null") + else str(scope_raw) + ) audience_raw = server_element.get("audience") audience = ( - None if not audience_raw or audience_raw.lower() == "default" else audience_raw + None + if not audience_raw + or (isinstance(audience_raw, str) and audience_raw.lower() == "default") + else str(audience_raw) ) + publisher_raw = server_element.get("publisher") + publisher = str(publisher_raw) if publisher_raw is not None else None + return MCPServerConfig( mcp_server_name=mcp_server_name, mcp_server_unique_name=mcp_server_unique_name, url=final_url, audience=audience, scope=scope, - publisher=server_element.get("publisher"), + publisher=publisher, ) - except Exception: + except Exception as exc: + self._logger.warning( + f"Failed to parse server config from element {server_element!r}: {exc}" + ) return None # -------------------------------------------------------------------------- # VALIDATION AND UTILITY HELPERS # -------------------------------------------------------------------------- - def _validate_input_parameters(self, agentic_app_id: str, auth_token: str) -> None: + def _validate_input_parameters(self, agentic_app_id: str, auth_token: Optional[str]) -> None: """ Validates input parameters for the main API method. + In development mode, servers are loaded from ToolingManifest.json rather than + the gateway, so neither ``agentic_app_id`` nor ``auth_token`` is required. + Validation is therefore skipped in dev mode to allow token-free local development. + Args: - agentic_app_id: Agentic App ID to validate. - auth_token: Authentication token to validate. + agentic_app_id: Agentic App ID to validate (required in production). + auth_token: Authentication token to validate (required in production). Raises: - ValueError: If any parameter is invalid or empty. + ValueError: If any required parameter is invalid or empty (production only). """ + if self._is_development_scenario(): + return if not agentic_app_id: raise ValueError("agentic_app_id cannot be empty or None") if not auth_token: raise ValueError("auth_token cannot be empty or None") - def _extract_server_name(self, server_element: Dict[str, Any]) -> Optional[str]: + def _extract_server_name(self, server_element: Dict[str, object]) -> Optional[str]: """ Extracts server name from configuration element. @@ -788,30 +770,29 @@ def _extract_server_name(self, server_element: Dict[str, Any]) -> Optional[str]: Returns: Server name string or None. """ - if "mcpServerName" in server_element and isinstance(server_element["mcpServerName"], str): - return server_element["mcpServerName"] - return None + value = server_element.get("mcpServerName") + return value if isinstance(value, str) else None - def _extract_server_unique_name(self, server_element: Dict[str, Any]) -> Optional[str]: + def _extract_server_unique_name(self, server_element: Dict[str, object]) -> Optional[str]: """ Extracts server unique name from configuration element. + Falls back to ``mcpServerName`` when ``mcpServerUniqueName`` is absent. + Args: server_element: Configuration dictionary. Returns: Server unique name string or None. """ - if "mcpServerUniqueName" in server_element and isinstance( - server_element["mcpServerUniqueName"], str - ): - return server_element["mcpServerUniqueName"] + value = server_element.get("mcpServerUniqueName") + if isinstance(value, str): + return value # Fall back to mcpServerName when mcpServerUniqueName is absent - if "mcpServerName" in server_element and isinstance(server_element["mcpServerName"], str): - return server_element["mcpServerName"] - return None + fallback = server_element.get("mcpServerName") + return fallback if isinstance(fallback, str) else None - def _extract_server_url(self, server_element: Dict[str, Any]) -> Optional[str]: + def _extract_server_url(self, server_element: Dict[str, object]) -> Optional[str]: """ Extracts custom server URL from configuration element. @@ -821,14 +802,12 @@ def _extract_server_url(self, server_element: Dict[str, Any]) -> Optional[str]: Returns: Server URL string or None. """ - # Check for 'url' field in both manifest and gateway responses - if "url" in server_element and isinstance(server_element["url"], str): - return server_element["url"] - return None + value = server_element.get("url") + return value if isinstance(value, str) else None def _validate_server_strings(self, name: Optional[str], unique_name: Optional[str]) -> bool: """ - Validates that server name and unique name are valid strings. + Validates that server name and unique name are non-empty strings. Args: name: Server name to validate. @@ -837,7 +816,12 @@ def _validate_server_strings(self, name: Optional[str], unique_name: Optional[st Returns: True if both strings are valid, False otherwise. """ - return name is not None and name.strip() and unique_name is not None and unique_name.strip() + return ( + name is not None + and bool(name.strip()) + and unique_name is not None + and bool(unique_name.strip()) + ) # -------------------------------------------------------------------------- # SEND CHAT HISTORY diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index 813fbce7..2ca8c700 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -68,6 +68,34 @@ def build_mcp_server_url(server_name: str) -> str: return f"{base_url}/{server_name}" +def is_development_environment() -> bool: + """ + Returns True if the current environment is configured as development. + + Resolution order (first non-empty value wins): + 1. ``PYTHON_ENVIRONMENT`` — explicit Python SDK variable used in current samples. + 2. ``ENVIRONMENT`` — legacy Python SDK variable (backward compatibility). + 3. ``ASPNETCORE_ENVIRONMENT`` — .NET / Azure hosting convention. + 4. ``DOTNET_ENVIRONMENT`` — .NET generic-host convention. + 5. Defaults to ``"Development"`` when none of the above are set. + + ``PYTHON_ENVIRONMENT`` and ``ENVIRONMENT`` are checked before the .NET variables + so legacy Python agents that set ``ENVIRONMENT=Production`` are not affected if an + unrelated process also sets ``ASPNETCORE_ENVIRONMENT`` (e.g. a .NET sidecar). + + Returns: + bool: True when the resolved environment is "development" (case-insensitive). + """ + environment = ( + os.getenv("PYTHON_ENVIRONMENT") + or os.getenv("ENVIRONMENT") + or os.getenv("ASPNETCORE_ENVIRONMENT") + or os.getenv("DOTNET_ENVIRONMENT") + or "Development" + ) + return environment.lower() == "development" + + def _get_current_environment() -> str: """ Gets the current environment name. @@ -135,15 +163,20 @@ def resolve_token_scope_for_server(server: MCPServerConfig) -> str: str: The OAuth scope string, e.g. ``"/Tools.ListInvoke.All"``, ``"api:///.default"``, or the shared ATG ``"/.default"``. """ - if ( - server.audience is not None - and server.audience.lower() != "default" - and server.audience != ATG_APP_ID - and server.audience != ATG_APP_ID_URI - ): - # V2: use explicit scope when present, fall back to /.default (pre-consented) - if server.scope: - return f"{server.audience}/{server.scope}" - return f"{server.audience}/.default" - # V1: shared ATG platform token - return f"{ATG_APP_ID}/.default" + if server.audience is not None: + # Normalize once: strip whitespace and lowercase so that GUID casing differences + # (e.g. "EA9FFC3E-..." vs "ea9ffc3e-...") and api:// scheme variations do not + # misclassify V1 servers as V2 or produce inconsistent OAuth cache keys. + audience = server.audience.strip().lower() + if ( + audience != "default" + and audience != ATG_APP_ID # already lowercase constant + and audience != ATG_APP_ID_URI # already lowercase constant + ): + # V2: use explicit scope when present, fall back to /.default (pre-consented). + # Use the normalized audience so scope strings are consistent cache keys. + if server.scope: + return f"{audience}/{server.scope}" + return f"{audience}/.default" + # V1: shared ATG platform token, configurable via MCP_PLATFORM_AUTHENTICATION_SCOPE env var + return get_mcp_platform_authentication_scope()[0] diff --git a/tests/tooling/extensions/googleadk/test_mcp_tool_registration_service.py b/tests/tooling/extensions/googleadk/test_mcp_tool_registration_service.py index 7b3b3b98..db9a8667 100644 --- a/tests/tooling/extensions/googleadk/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/googleadk/test_mcp_tool_registration_service.py @@ -3,6 +3,7 @@ """Unit tests for McpToolRegistrationService in Google ADK extension.""" +import os from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -102,10 +103,11 @@ def mock_server_config(self): @pytest.mark.asyncio @pytest.mark.unit + @patch.dict(os.environ, {"ENVIRONMENT": "Production"}) async def test_add_tool_servers_exchanges_token_when_not_provided( self, mock_agent, mock_authorization, mock_turn_context ): - """Test that token is exchanged when not provided.""" + """Test that token is exchanged when not provided (production mode only).""" with ( patch( "microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.McpToolServerConfigurationService" diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 928907e3..27f93da4 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -95,7 +95,7 @@ def test_parse_manifest_server_config_with_custom_url(self, service): "url": "https://my.custom.server/mcp", } - config = service._parse_manifest_server_config(server_element) + config = service._parse_server_config(server_element) assert config is not None assert config.mcp_server_name == "CustomServer" @@ -114,7 +114,7 @@ def test_parse_manifest_server_config_without_custom_url(self, mock_build_url, s "mcpServerUniqueName": "test_server", } - config = service._parse_manifest_server_config(server_element) + config = service._parse_server_config(server_element) assert config is not None assert config.mcp_server_name == "DefaultServer" @@ -132,7 +132,7 @@ def test_parse_gateway_server_config_with_custom_url(self, service): "url": "https://gateway.custom.url/mcp", } - config = service._parse_gateway_server_config(server_element) + config = service._parse_server_config(server_element) assert config is not None assert config.mcp_server_name == "GatewayServer" @@ -151,7 +151,7 @@ def test_parse_gateway_server_config_without_custom_url(self, mock_build_url, se "mcpServerUniqueName": "gateway_server", } - config = service._parse_gateway_server_config(server_element) + config = service._parse_server_config(server_element) assert config is not None assert config.mcp_server_name == "GatewayServer" @@ -202,13 +202,15 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u mock_gateway_url.return_value = "https://gateway.test/agents/test-app-id/mcpServers" # Mock aiohttp response + # V1 server (no audience) — this test verifies URL preservation, not token exchange. + # V2 servers (with a non-ATG audience) require auth context to be passed; that + # behaviour is tested separately in the per-audience token tests. mock_response_data = { "mcpServers": [ { "mcpServerName": "ProdServer", "mcpServerUniqueName": "prod_server", "url": "https://prod.custom.url/mcp", - "audience": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", } ] } @@ -217,7 +219,7 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u # Create proper async context managers mock_response = MagicMock() mock_response.status = 200 - mock_response.text = AsyncMock(return_value=json.dumps(mock_response_data)) + mock_response.json = AsyncMock(return_value=mock_response_data) # Create async context manager for response mock_response_cm = MagicMock() @@ -242,7 +244,6 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u assert servers[0].mcp_server_name == "ProdServer" assert servers[0].mcp_server_unique_name == "prod_server" assert servers[0].url == "https://prod.custom.url/mcp" - assert servers[0].audience == "a1b2c3d4-e5f6-7890-abcd-ef1234567890" class TestResolveTokenScopeForServer: @@ -495,6 +496,45 @@ async def test_preserves_existing_server_headers(self, service): assert result[0].headers["X-Custom"] == "my-value" assert result[0].headers["Authorization"] == "Bearer tok" + @pytest.mark.asyncio + @pytest.mark.parametrize( + "env_value", + [ + "Bearer rawtoken123", + "bearer rawtoken123", + "BEARER rawtoken123", + "BeArEr rawtoken123", + ], + ) + async def test_dev_acquirer_strips_bearer_prefix(self, service, env_value): + """Dev acquirer strips an existing 'Bearer ' prefix to prevent doubled headers.""" + server = self._make_server("mail") + with patch.dict(os.environ, {"BEARER_TOKEN": env_value}): + acquire = service._create_dev_token_acquirer() + result = await service._attach_per_audience_tokens([server], acquire) + + assert result[0].headers["Authorization"] == "Bearer rawtoken123" + + @pytest.mark.asyncio + async def test_dev_acquirer_raw_token_unchanged(self, service): + """Dev acquirer leaves a raw token (no prefix) unchanged.""" + server = self._make_server("mail") + with patch.dict(os.environ, {"BEARER_TOKEN": "rawtoken456"}): + acquire = service._create_dev_token_acquirer() + result = await service._attach_per_audience_tokens([server], acquire) + + assert result[0].headers["Authorization"] == "Bearer rawtoken456" + + @pytest.mark.asyncio + async def test_dev_acquirer_per_server_token_strips_bearer_prefix(self, service): + """Per-server BEARER_TOKEN_ env var also has its Bearer prefix stripped.""" + server = self._make_server("mail") + with patch.dict(os.environ, {"BEARER_TOKEN_MAIL": "Bearer per-server-tok"}): + acquire = service._create_dev_token_acquirer() + result = await service._attach_per_audience_tokens([server], acquire) + + assert result[0].headers["Authorization"] == "Bearer per-server-tok" + class TestPrepareGatewayHeaders: """Tests for _prepare_gateway_headers and _resolve_agent_id_for_header.""" From 6f340d63c577b5ae25d351a1307c41f5902d9db0 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 16:36:43 +0530 Subject: [PATCH 12/19] 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. --- .../services/mcp_tool_registration_service.py | 19 +- .../pyproject.toml | 2 +- .../mcp_tool_server_configuration_service.py | 13 +- pyproject.toml | 5 +- .../test_mcp_tool_registration_service.py | 2 +- .../services/test_send_chat_history.py | 2 +- .../tooling/test_mcp_server_configuration.py | 1 - uv.lock | 1091 ++++++++++++++++- 8 files changed, 1082 insertions(+), 53 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py index 2d2ff42c..3704e89d 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py @@ -4,10 +4,9 @@ import logging import uuid from datetime import datetime, timezone -from typing import List, Optional, Sequence, Union +from typing import List, Optional, Sequence -from agent_framework import RawAgent, Message, BaseHistoryProvider, MCPStreamableHTTPTool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import RawAgent, Message, HistoryProvider, MCPStreamableHTTPTool from agent_framework.openai import OpenAIChatClient import httpx @@ -56,7 +55,7 @@ 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[object], auth: Authorization, @@ -68,7 +67,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 @@ -91,7 +90,9 @@ async def add_tool_servers_to_agent( auth_token = authToken.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) + 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}") @@ -320,18 +321,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. diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml index f0b6e125..d3b746a8 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml @@ -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", diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 66037dbe..8204e879 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -217,11 +217,7 @@ def _is_v2_server(self, server: MCPServerConfig) -> bool: if server.audience is None: return False audience = server.audience.strip().lower() - return ( - audience != "default" - and audience != ATG_APP_ID - and audience != ATG_APP_ID_URI - ) + return audience != "default" and audience != ATG_APP_ID and audience != ATG_APP_ID_URI def _create_dev_token_acquirer(self) -> TokenAcquirer: """ @@ -673,9 +669,7 @@ async def _parse_gateway_response( # CONFIGURATION PARSING HELPERS # -------------------------------------------------------------------------- - def _parse_server_config( - self, server_element: Dict[str, object] - ) -> Optional[MCPServerConfig]: + def _parse_server_config(self, server_element: Dict[str, object]) -> Optional[MCPServerConfig]: """ Parses a server configuration from manifest or gateway response data. @@ -703,8 +697,7 @@ def _parse_server_config( scope_raw = server_element.get("scope") scope = ( None - if not scope_raw - or (isinstance(scope_raw, str) and scope_raw.lower() == "null") + if not scope_raw or (isinstance(scope_raw, str) and scope_raw.lower() == "null") else str(scope_raw) ) diff --git a/pyproject.toml b/pyproject.toml index 5f3cc03d..a90176b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dev-dependencies = [ "tox-uv>=1.0", "python-dotenv", "openai", - "agent-framework-azure-ai", + "agent-framework", "azure-identity", "openai-agents", ] @@ -85,7 +85,8 @@ constraint-dependencies = [ "azure-monitor-opentelemetry-exporter >= 1.0.0b39", # --- AI Frameworks --- - "agent-framework-azure-ai >= 1.0.0rc4", + "agent-framework >= 1.0.0", + "agent-framework-core >= 1.0.0", "langchain >= 0.1.0", "langchain-core >= 0.1.0", "openai-agents >= 0.2.6", diff --git a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py index 5066c0a1..d67b0577 100644 --- a/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/agentframework/services/test_mcp_tool_registration_service.py @@ -52,7 +52,7 @@ def mock_auth(self): @pytest.fixture def mock_chat_client(self): - """Create a mock OpenAIChatClient or AzureOpenAIChatClient.""" + """Create a mock OpenAIChatClient.""" return Mock() @pytest.fixture diff --git a/tests/tooling/extensions/agentframework/services/test_send_chat_history.py b/tests/tooling/extensions/agentframework/services/test_send_chat_history.py index 013fe466..985baec9 100644 --- a/tests/tooling/extensions/agentframework/services/test_send_chat_history.py +++ b/tests/tooling/extensions/agentframework/services/test_send_chat_history.py @@ -65,7 +65,7 @@ def sample_chat_messages(self, mock_role, mock_assistant_role): @pytest.fixture def mock_chat_message_store(self, sample_chat_messages): - """Create a mock BaseHistoryProvider.""" + """Create a mock HistoryProvider.""" store = AsyncMock() store.get_messages = AsyncMock(return_value=sample_chat_messages) return store diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 27f93da4..9ac738da 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -3,7 +3,6 @@ """Unit tests for MCP Server Configuration Service.""" -import json import os from typing import Any from unittest.mock import AsyncMock, MagicMock, patch diff --git a/uv.lock b/uv.lock index 653d6628..7c6df9d5 100644 --- a/uv.lock +++ b/uv.lock @@ -26,7 +26,8 @@ members = [ "microsoft-agents-a365-tooling-extensions-semantickernel", ] constraints = [ - { name = "agent-framework-azure-ai", specifier = ">=1.0.0rc4" }, + { name = "agent-framework", specifier = ">=1.0.0" }, + { name = "agent-framework-core", specifier = ">=1.0.0" }, { name = "aiohttp", specifier = ">=3.8.0" }, { name = "asyncio-throttle", specifier = ">=1.0.0" }, { name = "azure-ai-agents", specifier = ">=1.0.0b251001" }, @@ -67,7 +68,7 @@ overrides = [{ name = "azure-ai-projects", specifier = ">=2.0.0b1" }] [manifest.dependency-groups] dev = [ - { name = "agent-framework-azure-ai" }, + { name = "agent-framework" }, { name = "azure-identity" }, { name = "openai" }, { name = "openai-agents" }, @@ -82,6 +83,22 @@ dev = [ { name = "tox-uv", specifier = ">=1.0" }, ] +[[package]] +name = "a2a-sdk" +version = "0.3.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/6a/2fe24e0a85240a651006c12f79bdb37156adc760a96c44bc002ebda77916/a2a_sdk-0.3.23.tar.gz", hash = "sha256:7c46b8572c4633a2b41fced2833e11e62871e8539a5b3c782ba2ba1e33d213c2", size = 255265, upload-time = "2026-02-17T08:34:34.648Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/20/77d119f19ab03449d3e6bc0b1f11296d593dae99775c1d891ab1e290e416/a2a_sdk-0.3.23-py3-none-any.whl", hash = "sha256:8c2f01dffbfdd3509eafc15c4684743e6ae75e69a5df5d6f87be214c948e7530", size = 145689, upload-time = "2026-02-17T08:34:33.263Z" }, +] + [[package]] name = "absolufy-imports" version = "0.3.1" @@ -92,40 +109,380 @@ wheels = [ ] [[package]] -name = "agent-framework-azure-ai" -version = "1.0.0rc4" +name = "ag-ui-protocol" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b5/fc0b65b561d00d88811c8a7d98ee735833f81554be244340950e7b65820c/ag_ui_protocol-0.1.13.tar.gz", hash = "sha256:811d7d7dcce4783dec252918f40b717ebfa559399bf6b071c4ba47c0c1e21bcb", size = 5671, upload-time = "2026-02-19T18:40:38.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/9f/b833c1ab1999da35ebad54841ae85d2c2764c931da9a6f52d8541b6901b2/ag_ui_protocol-0.1.13-py3-none-any.whl", hash = "sha256:1393fa894c1e8416efe184168a50689e760d05b32f4646eebb8ff423dddf8e8f", size = 8053, upload-time = "2026-02-19T18:40:37.27Z" }, +] + +[[package]] +name = "agent-framework" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core", extra = ["all"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/0f/dcedaa3520c9a3b850d88b08846d518d10daec02c8eabc18c7b271bc4d28/agent_framework-1.0.1.tar.gz", hash = "sha256:163c319c7d37119849447a9f7e9fab4e0b2d0195523b82d3748f78b78ff97343", size = 4361213, upload-time = "2026-04-10T03:30:59.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/9d/a5d278effe7a5ffcc515bf870a9a00704391f0506a1fba11ec3b582ef11c/agent_framework-1.0.1-py3-none-any.whl", hash = "sha256:5f8184613b129363106fe6e04db26075b2d2eca0026da9770dc92bbd9e4a45d6", size = 5686, upload-time = "2026-04-10T03:31:03.825Z" }, +] + +[[package]] +name = "agent-framework-a2a" +version = "1.0.0b260409" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "a2a-sdk" }, { name = "agent-framework-core" }, - { name = "aiohttp" }, - { name = "azure-ai-agents" }, - { name = "azure-ai-inference" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/d4/2641d0584c5859f0054207d0a726a698d82eb3c8cba1d5f9d6d7fcf785ec/agent_framework_azure_ai-1.0.0rc4.tar.gz", hash = "sha256:c397f1bb74d29be4e5842e0989f2006f981f77f7066533899bf977fc79f6e046", size = 48428, upload-time = "2026-03-11T23:19:30.131Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/b6a98dfc08fd3ed39afce60004a29d279286d69dff18c5a493478e1211d8/agent_framework_a2a-1.0.0b260409.tar.gz", hash = "sha256:107d8120e632aff0a6c7b19ee95a6b14f3bd9fb0d992b0737474b4c5c6fef0b3", size = 10184, upload-time = "2026-04-10T03:30:53.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/73/5780db99a1e732788749e619cb61b04741d00981dea2e104ec5359116a78/agent_framework_a2a-1.0.0b260409-py3-none-any.whl", hash = "sha256:ff039d622f3a59706464e8e32ab22e94cf98ccd89d67a87be5d69062195e5adc", size = 10035, upload-time = "2026-04-10T03:26:25.177Z" }, +] + +[[package]] +name = "agent-framework-ag-ui" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ag-ui-protocol" }, + { name = "agent-framework-core" }, + { name = "fastapi" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/05/8441c47586a4ace62a735ce6dc5159e7b968e00e8bfafe4624f0c0d055e3/agent_framework_ag_ui-1.0.0b260409.tar.gz", hash = "sha256:e666d61c8bde8787e8bae3e434e2d93be04234d263dc50a9228a26a5517a1845", size = 171851, upload-time = "2026-04-10T03:26:24.131Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/91/d1ab5483001a5ef2ac37df23303b5eddc6c3c25b081d90571df7110d345e/agent_framework_ag_ui-1.0.0b260409-py3-none-any.whl", hash = "sha256:28094773d2287f5e5b25d8d5c165e5eb81c0b2a88348f5164e4f5a11ab9c260b", size = 91757, upload-time = "2026-04-10T03:26:14.453Z" }, +] + +[[package]] +name = "agent-framework-anthropic" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "anthropic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/e9b70503084a808adc01f1939d59438439243ad3b536f09cf7fdc432532e/agent_framework_anthropic-1.0.0b260409.tar.gz", hash = "sha256:4db8ea132ee161e0c3d957d9becdf99474617ad80d82a17072b696fbc4d988a8", size = 18323, upload-time = "2026-04-10T03:30:49.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/a6/a68c7a964cd8a27ef22c4984860d1c04c0ae00dd289ebcb4c57495a79101/agent_framework_anthropic-1.0.0b260409-py3-none-any.whl", hash = "sha256:99aabcf1eabd4d09a8f3a75a1b60bd1ce11e81d735ac5ad8c949dba19d738ae5", size = 21094, upload-time = "2026-04-10T03:26:19.301Z" }, +] + +[[package]] +name = "agent-framework-azure-ai-search" +version = "0.0.0a1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/58/b9c706e03b3407be3c70777124136cf428f7879664f9032e606d23024208/agent_framework_azure_ai_search-0.0.0a1.tar.gz", hash = "sha256:ca60fa77a8c3a55eb954c03de4b74ecf890566220854acaad4e07d56f86f43be", size = 1658, upload-time = "2025-09-30T01:34:23.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/c0/bd014d57a6718272a10955e679a7e08307cabd9557925350ca6e5f94eae9/agent_framework_azure_ai_search-0.0.0a1-py3-none-any.whl", hash = "sha256:b913cb4640a6a2539b1a008462f6dbdca64b14ad9c2bd68a99fa396b5312e876", size = 2373, upload-time = "2025-09-30T01:34:21.349Z" }, +] + +[[package]] +name = "agent-framework-azure-cosmos" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-cosmos" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/3a/f3a87c8a8bc5bde32a225bb4e982d9cca80b3890bb461b242ae96408d4bf/agent_framework_azure_cosmos-1.0.0b260409.tar.gz", hash = "sha256:0584baacfd66b9c72fb25cfd4d14a8fd846b37290d0cec9df8b487ce91c2a8e6", size = 10577, upload-time = "2026-04-10T03:30:45.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/e2/92f195daf709ec5af39302c1c50cbe580e25df3f4b1e6229927a2d4d58ba/agent_framework_azure_cosmos-1.0.0b260409-py3-none-any.whl", hash = "sha256:10882ed4f7aefb69351c5ac33168333f060e27e54da080f7a3bb90a267932e3c", size = 11584, upload-time = "2026-04-10T03:26:32.362Z" }, +] + +[[package]] +name = "agent-framework-azurefunctions" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-durabletask" }, + { name = "azure-functions" }, + { name = "azure-functions-durable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/68/758f6adf7ae08596cf8fe07fb185ad3bd134574c6db8b0d790334374faab/agent_framework_azurefunctions-1.0.0b260409.tar.gz", hash = "sha256:813b4c3b70f1d61e24e817fe4f471484feff7bbc79d60335cadc89182aca2d41", size = 32369, upload-time = "2026-04-10T03:29:26.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/7a/b475e49fa313f9176cbcdfeb80141ad76ce20b45e07904bf023f6c96399b/agent_framework_azurefunctions-1.0.0b260409-py3-none-any.whl", hash = "sha256:3c11ca28defa120efb6ac07f11adcb89a9f8bb704a3142752579d455ca299beb", size = 35491, upload-time = "2026-04-10T03:30:46.854Z" }, +] + +[[package]] +name = "agent-framework-bedrock" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "boto3" }, + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/f2/c901fdad90cfb8c59ba42ec889ea5fac5bfee2a370f06566745388c6dddc/agent_framework_bedrock-1.0.0b260409.tar.gz", hash = "sha256:cdf139738de85ffd9e16992e44217fa97519800419d58ce9ce2846e26ae5963c", size = 17037, upload-time = "2026-04-10T03:30:51.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/85/42edfbf6e16548df63d8eed249ab78ec41f1655cbf8e7181afd36e5f8d6a/agent_framework_bedrock-1.0.0b260409-py3-none-any.whl", hash = "sha256:a00d375d127aa09321095b0aba1622a6094004c2043a374fd0e5c38a99beca82", size = 13834, upload-time = "2026-04-10T03:30:55.447Z" }, +] + +[[package]] +name = "agent-framework-chatkit" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "openai-chatkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/5f/ccdfe2b3f7a925d10379c872faf97cfd9028e34b9ca5f905c8c31c8e6aa5/agent_framework_chatkit-1.0.0b260409.tar.gz", hash = "sha256:df21d2f1f482424e09dc22927be998e867e44b10ff4ff6e49f9553425eb9d47a", size = 12525, upload-time = "2026-04-10T03:30:42.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/99/e3dc4594c99bac27e6154ddf25f9380a8cfaa00bf2c55043071fec06593b/agent_framework_chatkit-1.0.0b260409-py3-none-any.whl", hash = "sha256:9f19ef28df6fb9d03161d5cc0ea98842e24a63e0089aca3612c0c21f322e37ba", size = 11722, upload-time = "2026-04-10T03:31:04.639Z" }, +] + +[[package]] +name = "agent-framework-claude" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "claude-agent-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/c4/b49a2ed9b233ee0a603b81ac866d6868a29e5420666af9bf267445056c67/agent_framework_claude-1.0.0b260409.tar.gz", hash = "sha256:67a49d8c7e9885214498795604e1fd5bb515c628e612ea94361cfefe66bc9d09", size = 10323, upload-time = "2026-04-10T03:26:31.241Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/aa/77d529aebaab8fc93a5214003269d0f8459db66b689fdcd353bbe20ac85c/agent_framework_claude-1.0.0b260409-py3-none-any.whl", hash = "sha256:ac29bf9a79adf95484247d07e516cfa834a6cc1de299b94711e9c49fe44c9c30", size = 10400, upload-time = "2026-04-10T03:30:57.316Z" }, +] + +[[package]] +name = "agent-framework-copilotstudio" +version = "1.0.0b260311" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "microsoft-agents-copilotstudio-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/29/6360d93ce6d9b3fac21b2ef23515027e18f0a520f805fbc710a83bb81b61/agent_framework_copilotstudio-1.0.0b260311.tar.gz", hash = "sha256:035b25e6af772a65263b2f80ab4c46b63995a6325aa04c8a12e7f97142999afa", size = 8466, upload-time = "2026-03-11T23:19:30.891Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8c/703220347d2a656c0979dbb7e788a851e3cc7e6396ff6402a4606a0d7555/agent_framework_azure_ai-1.0.0rc4-py3-none-any.whl", hash = "sha256:538c6782a06dcb9df0631379b776018b6b0ddb81804d142eb3787c36a42ab2c8", size = 54269, upload-time = "2026-03-11T23:20:04.856Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/8e4a47c2dbba44b697831aa3a8efbabe9dbce2c76ae9eda5f8aa427a16da/agent_framework_copilotstudio-1.0.0b260311-py3-none-any.whl", hash = "sha256:ba9fde502c96d4e0eb68951df843947248d4aacbc5d0dcf445c685361f94ad97", size = 8563, upload-time = "2026-03-11T23:20:01.274Z" }, ] [[package]] name = "agent-framework-core" -version = "1.0.0rc4" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "azure-ai-projects" }, - { name = "azure-identity" }, - { name = "mcp", extra = ["ws"] }, - { name = "openai" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions-ai" }, - { name = "packaging" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/5a/b472f9a57235bb72899151ec5cd3c925825e16018689e0300cb822cf00f8/agent_framework_core-1.0.0rc4.tar.gz", hash = "sha256:f394eb95ae877ae854aa7a3e499f76f34b26102808009a66b264ded89c6b6dbd", size = 302446, upload-time = "2026-03-11T23:19:29.198Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/3d/371e57a74ecd4fc551d458bd234d7591c052b467cac21e8805cb519a4187/agent_framework_core-1.0.1.tar.gz", hash = "sha256:6ace9fa8bee9d2e8556c28ff767d89b8e0a0a734246dcca4a196d0b0bc5cedb0", size = 285179, upload-time = "2026-04-10T03:29:28.193Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/d7/89776e7e919e46fd83ae464a416966715f4f40083297d42574e3d45214f6/agent_framework_core-1.0.0rc4-py3-none-any.whl", hash = "sha256:f01a6997be0f5e05853eb6be341dbca692c4e5d6999de5f3e8364296de50635f", size = 348882, upload-time = "2026-03-11T23:19:43.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/11/a460d6656257c4302deb33724f29a52059bae193758ded7571fb576b26cb/agent_framework_core-1.0.1-py3-none-any.whl", hash = "sha256:8305fadb78adb9b625cda0ba8188bcb76ce01a2aa64eed937f8c9fb384043bc0", size = 323596, upload-time = "2026-04-10T03:31:01.698Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "agent-framework-a2a" }, + { name = "agent-framework-ag-ui" }, + { name = "agent-framework-anthropic" }, + { name = "agent-framework-azure-ai-search" }, + { name = "agent-framework-azure-cosmos" }, + { name = "agent-framework-azurefunctions" }, + { name = "agent-framework-bedrock" }, + { name = "agent-framework-chatkit" }, + { name = "agent-framework-claude" }, + { name = "agent-framework-copilotstudio" }, + { name = "agent-framework-declarative" }, + { name = "agent-framework-devui" }, + { name = "agent-framework-durabletask" }, + { name = "agent-framework-foundry" }, + { name = "agent-framework-foundry-local" }, + { name = "agent-framework-github-copilot" }, + { name = "agent-framework-lab" }, + { name = "agent-framework-mem0" }, + { name = "agent-framework-ollama" }, + { name = "agent-framework-openai" }, + { name = "agent-framework-orchestrations" }, + { name = "agent-framework-purview" }, + { name = "agent-framework-redis" }, + { name = "mcp" }, +] + +[[package]] +name = "agent-framework-declarative" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "powerfx", marker = "python_full_version < '3.14'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/6d/7a14833f74cbaefb055ca6a3714a48347da206be62363de50632a1107c77/agent_framework_declarative-1.0.0b260409.tar.gz", hash = "sha256:b29b509b62416231dda6b98a5832503e73362b03366016a623ae781e200db021", size = 69910, upload-time = "2026-04-10T03:30:52.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/9c/ca35fbec4be4463a307468a5014ee4153cbf94ed7d8cbb61470e2ab3298c/agent_framework_declarative-1.0.0b260409-py3-none-any.whl", hash = "sha256:8b667a45422e89ee80b0c918e6e1d98e35eb7933a3830830965a1eff7759b305", size = 78051, upload-time = "2026-04-10T03:26:30.125Z" }, +] + +[[package]] +name = "agent-framework-devui" +version = "1.0.0b260414" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "fastapi" }, + { name = "openai" }, + { name = "opentelemetry-sdk" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/55/5d745ea66a2f7eafe796b3908e4569ab8457114f69988e0594f030ab1f47/agent_framework_devui-1.0.0b260414.tar.gz", hash = "sha256:c2799d61ad624b21bc2c83a06ac095fbc219b443fb86d2bd8398a3bb0fef93d2", size = 358047, upload-time = "2026-04-15T21:22:32.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ef/5f66cbfceb219cd11c124e950b9104319a16146e8ec0d64f8e610d95dd47/agent_framework_devui-1.0.0b260414-py3-none-any.whl", hash = "sha256:61eaf57dd61a99492231c1e7d106fd79e875c35042ca331e777009958ef08319", size = 362983, upload-time = "2026-04-15T21:22:30.594Z" }, +] + +[[package]] +name = "agent-framework-durabletask" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "durabletask" }, + { name = "durabletask-azuremanaged" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/15/785100eee6ac61cd4cd29ff6690f43eebc15152d888580b0c00c06d5e779/agent_framework_durabletask-1.0.0b260409.tar.gz", hash = "sha256:0425f3afccea7b13a75fe52c3b59c40a6f5951bb30eca5fb4c1baad442eaa82d", size = 30596, upload-time = "2026-04-10T03:26:09.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/05/a6616a82e36928c79178ad583ce2e1db787a0fd9c24157e83b86a6c86a98/agent_framework_durabletask-1.0.0b260409-py3-none-any.whl", hash = "sha256:cee3c29b22d5e87af29d349134c789a475c2f89105243b2b15ec141256b4d4cd", size = 36736, upload-time = "2026-04-10T03:30:50.736Z" }, +] + +[[package]] +name = "agent-framework-foundry" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "azure-ai-inference" }, + { name = "azure-ai-projects" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/66/bd7b45bbfc6cebb261b291bae7e7030e9a81acf449d4e16257acc7b349d5/agent_framework_foundry-1.0.1.tar.gz", hash = "sha256:7a1101fcab51c71a544324317a933c8fae397b8ddce9136d56e6ec6ff5a774a6", size = 26449, upload-time = "2026-04-10T04:46:39.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/3f/e99c0acb7c2ed64bc948ae7676920c1576865ee8b1e46cf378b27b0de20b/agent_framework_foundry-1.0.1-py3-none-any.whl", hash = "sha256:494bab12300d364ade0de738f12d7509de9536da355182067e2c00758ea17cf4", size = 30705, upload-time = "2026-04-10T04:46:38.188Z" }, +] + +[[package]] +name = "agent-framework-foundry-local" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "foundry-local-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/99/5decac73c55677643161cca1cf030547c55a3243348460b82a0f7fd00c7b/agent_framework_foundry_local-1.0.0b260409.tar.gz", hash = "sha256:0f794021779398e193e076668436a065de7e3a66dde36c0b996a048a9c96dd6e", size = 6658, upload-time = "2026-04-10T03:26:13.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f7/b54292a1bfe58ac5448b85d2e530ecd57204d99e8933be85c80e151174ec/agent_framework_foundry_local-1.0.0b260409-py3-none-any.whl", hash = "sha256:a411756395c5090bdb70fa2d5a22d3fdc6b098aa3e193c86f13772232b442903", size = 7160, upload-time = "2026-04-10T03:26:16.881Z" }, +] + +[[package]] +name = "agent-framework-github-copilot" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "github-copilot-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ac/b1716ec6e4163d186c6aafffafbf24cb057ffe24724f68bfaccf88881279/agent_framework_github_copilot-1.0.0b260409.tar.gz", hash = "sha256:a9d097a6ac205bb7eb7aabdc56df7c15869c7bf9133502c70220171e6cde2caa", size = 9854, upload-time = "2026-04-10T03:26:12.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/bb/d4a58a6f5cb37c9abdbb6319fb0c6e1e13011f7c10d19d20ebf14f45ac03/agent_framework_github_copilot-1.0.0b260409-py3-none-any.whl", hash = "sha256:dec44490d61e98cfd8d715e40c71b7ae6b615341666314b555d32f4efd41bcd5", size = 9962, upload-time = "2026-04-10T03:26:20.321Z" }, +] + +[[package]] +name = "agent-framework-lab" +version = "1.0.0b251024" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/c5/be86273cb3545651d0c8112ff9f38ae8fe13b740ce9b65b9be83ff2d70ee/agent_framework_lab-1.0.0b251024.tar.gz", hash = "sha256:4261cb595b6edfd4f30db613c1885c71b3dcfa2088cf29224d4f17b3ff956b2a", size = 23397, upload-time = "2025-10-24T18:13:48.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/0f/3974b2b1f6bf523ee3ced0886b6afd5ca8bbebd24aa5278ef77db0d3d765/agent_framework_lab-1.0.0b251024-py3-none-any.whl", hash = "sha256:1596408991a92fcacef4bb939305d2b59159517b707f48114105fc0dd46bfee7", size = 26589, upload-time = "2025-10-24T18:13:47.229Z" }, +] + +[[package]] +name = "agent-framework-mem0" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "mem0ai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/d5/ed4487705f85cad1aaf099798d0e76fbd4324d4e6eb681197f7c60b37038/agent_framework_mem0-1.0.0b260409.tar.gz", hash = "sha256:66f1989a3acc05c76acceac2f1c775f96f53d11c9099e562d91a20559d33303e", size = 5301, upload-time = "2026-04-10T03:26:21.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/04/a93bb2d18c7c2a305060baac2e03485abc1daffbe1b6a389991406e41e42/agent_framework_mem0-1.0.0b260409-py3-none-any.whl", hash = "sha256:9ef6766b512725e478779c177ce15cd61c1adb406880c88ad234bcb7a3cbcc4e", size = 5538, upload-time = "2026-04-10T03:30:56.291Z" }, +] + +[[package]] +name = "agent-framework-ollama" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "ollama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/91/d41350d1ff99eaac3eb37031be5d9b7b485f5f714636a44d91449b3dc969/agent_framework_ollama-1.0.0b260409.tar.gz", hash = "sha256:b91b3aba22003873c14a043bd9c8e2d2da051e972c3c2fcdae463bdc2c43fafd", size = 10233, upload-time = "2026-04-10T03:26:34.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/9b/be51a10c8fc09653f88536c854d9ab5d724797c647187267052fe714cb28/agent_framework_ollama-1.0.0b260409-py3-none-any.whl", hash = "sha256:711c4f19c03056a3a15b231cd7c55392038ed9e419d3cd9c2cdb0b2f67e2d932", size = 12046, upload-time = "2026-04-10T03:26:22.796Z" }, +] + +[[package]] +name = "agent-framework-openai" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/8b/8cae313b43f994815dc5be7195109bb05cd04ff40bcee2ec931d0b26d615/agent_framework_openai-1.0.1.tar.gz", hash = "sha256:d42913909ea4013d53cd3b95e17729c23e1349d64f45e5bc84b54292fc66ea82", size = 44469, upload-time = "2026-04-10T06:54:56.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c0/1281fdb5afa65b1e660673d7426a4aeb0548701ba6606041d76283d6a8e3/agent_framework_openai-1.0.1-py3-none-any.whl", hash = "sha256:c99c9d49c3de3cbe6a6dfa8fa5e157f0d6dc47baf7a78c66e6591c48c0c3cb37", size = 49338, upload-time = "2026-04-10T06:54:57.649Z" }, +] + +[[package]] +name = "agent-framework-orchestrations" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/e7/62b673ef3a16b157edf565b25bd4aa5cad2f7a2ae46ff0a21b46a572f8d4/agent_framework_orchestrations-1.0.0b260409.tar.gz", hash = "sha256:5146410546233eafefb4deaf404935f6d3d772a085e341325b7b644b5223927a", size = 54730, upload-time = "2026-04-10T03:26:26.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/4e/71832dd59764c79198f06b732dcf185d1296e970ef3ce55df9e8652b6e18/agent_framework_orchestrations-1.0.0b260409-py3-none-any.whl", hash = "sha256:361d65b6bfbb571b49428e0627637dc92fd3f56f3b5e1f1bbe8d32378c5aa24b", size = 60863, upload-time = "2026-04-10T03:30:48.787Z" }, +] + +[[package]] +name = "agent-framework-purview" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-core" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/8a/db3a63efabe27d11b4c9c2dfbc177cdbfd64f0660abb16d93db840b47303/agent_framework_purview-1.0.0b260409.tar.gz", hash = "sha256:92c4c6c4cbba538128eb8b496cb45403da1f53f240e46d1da580ffbfabb67916", size = 27822, upload-time = "2026-04-10T03:30:44.881Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/6b/8889cb3bd46abb569dea9ecfd5c5c539fa63a78ce0005070ba310eaae13c/agent_framework_purview-1.0.0b260409-py3-none-any.whl", hash = "sha256:a42436e763ddb15ae284d152b76322c2a5810e7789b03e7eb76124c5c5c9e6da", size = 27345, upload-time = "2026-04-10T03:30:47.929Z" }, +] + +[[package]] +name = "agent-framework-redis" +version = "1.0.0b260409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "numpy" }, + { name = "redis" }, + { name = "redisvl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/7c0e8987fa1a9107681a09a0cbac67536987d9ad136d23b896e6586b5d80/agent_framework_redis-1.0.0b260409.tar.gz", hash = "sha256:75a07adb57132b9704224127fc92ed598045a4d9e5c62045001a6b2b86d88333", size = 10515, upload-time = "2026-04-10T03:30:54.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/33/00cdb2bcbca47263ef939b706c97d38afed8ea74f745d164b21cfc7320e4/agent_framework_redis-1.0.0b260409-py3-none-any.whl", hash = "sha256:f7dc99f91c549501d5a52ce4175286727ba146638a6b413303322f8057a7af6b", size = 10596, upload-time = "2026-04-10T03:26:27.156Z" }, ] [[package]] @@ -315,6 +672,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/63/791e14ef5a8ecb485cef5b5d058c7ca3ad6c50a2f94cf4cea5231c6b7c16/anthropic-0.80.0.tar.gz", hash = "sha256:ef042586673fdcab2a6ffd381aa5f9a1bcce38ffe73c07fe70bd56d12b8124ba", size = 533291, upload-time = "2026-02-17T19:26:26.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/4b/665f29338f51d0c2f9e04b276ea54cc1e957ae5c521a0ad868aa80abc608/anthropic-0.80.0-py3-none-any.whl", hash = "sha256:dad0e40ec371ee686e9ffb2e0cb461a0ed51447fa100927fb5d39b174c286d6f", size = 453667, upload-time = "2026-02-17T19:26:29.96Z" }, +] + [[package]] name = "anyio" version = "4.12.1" @@ -328,6 +704,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "asyncio" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371, upload-time = "2025-08-05T02:51:46.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555, upload-time = "2025-08-05T02:51:45.767Z" }, +] + [[package]] name = "asyncio-throttle" version = "1.0.2" @@ -464,6 +858,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, ] +[[package]] +name = "azure-cosmos" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a3/0474e622bf9676e3206d61269461ed16a05958363c254ea3b15af16219b2/azure_cosmos-4.15.0.tar.gz", hash = "sha256:be1cf49837c197d9da880ec47fe020a24d679075b89e0e1e2aca8d376b3a5a24", size = 2100744, upload-time = "2026-02-23T16:01:52.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5f/b6e3d3ae16fa121fdc17e62447800d378b7e716cd6103c3650977a6c4618/azure_cosmos-4.15.0-py3-none-any.whl", hash = "sha256:83c1da7386bcd0df9a15c52116cc35012225d8a72d4f1379938b83ea5eb19fff", size = 424870, upload-time = "2026-02-23T16:01:54.514Z" }, +] + +[[package]] +name = "azure-functions" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/be/5535830e0658e9668093941b3c33b0ea03eceadbf6bd6b7870aa37ef071a/azure_functions-1.24.0.tar.gz", hash = "sha256:18ea1607c7a7268b7a1e1bd0cc28c5cc57a9db6baaacddb39ba0e9f865728187", size = 134495, upload-time = "2025-10-06T19:08:08.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/76/e6c5809ee0295e882b6c9ad595896748e33989d353b67316a854f65fb754/azure_functions-1.24.0-py3-none-any.whl", hash = "sha256:32b12c2a219824525849dd92036488edeb70d306d164efd9e941f10f9ac0a91c", size = 108341, upload-time = "2025-10-06T19:08:07.128Z" }, +] + +[[package]] +name = "azure-functions-durable" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-functions" }, + { name = "furl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/7c/3654377e7000c4bd6b6edbb959efc4ad867005353843a4d810dfa8fbb72b/azure_functions_durable-1.5.0.tar.gz", hash = "sha256:131fbdf08fa1140d94dc3948fcf9000d8da58aaa5a0ffc4db0ea3be97d5551e2", size = 183733, upload-time = "2026-02-04T20:33:45.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/25/fb054d81c1fda64b229b04b4051657fedd4a72f53c51c59fcaca3a454d2f/azure_functions_durable-1.5.0-py3-none-any.whl", hash = "sha256:aea683193328924ae56eebb8f80647e186baf93e26c061f09ce532702c279ddc", size = 146619, upload-time = "2026-02-04T20:33:16.838Z" }, +] + [[package]] name = "azure-identity" version = "1.25.1" @@ -526,6 +963,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499, upload-time = "2026-01-06T23:48:58.995Z" }, ] +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + [[package]] name = "black" version = "26.3.1" @@ -563,6 +1009,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] +[[package]] +name = "boto3" +version = "1.42.89" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/f7bccb22b245cabf392816baba20f9e95f78ace7dbc580fd40136e80e732/boto3-1.42.89.tar.gz", hash = "sha256:3e43aacc0801bba9bcd23a8c271c089af297a69565f783fcdd357ae0e330bf1e", size = 113165, upload-time = "2026-04-13T19:36:17.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/33/55103ba5ef9975ea54b8d39e69b76eb6e9fded3beae5f01065e26951a3a1/boto3-1.42.89-py3-none-any.whl", hash = "sha256:6204b189f4d0c655535f43d7eaa57ff4e8d965b8463c97e45952291211162932", size = 140556, upload-time = "2026-04-13T19:36:13.894Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.89" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/e6be943efa9051bd15c2ee14077c2b10d6e27c9e9385fc43a03a5c4ed8b5/botocore-1.42.89.tar.gz", hash = "sha256:95ac52f472dad29942f3088b278ab493044516c16dbf9133c975af16527baa99", size = 15206290, upload-time = "2026-04-13T19:36:02.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" }, +] + [[package]] name = "cachetools" version = "7.0.5" @@ -733,6 +1207,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/dd/2818538efd18ed4ef72d4775efa75bb36cbea0fa418eda51df85ee9c2424/claude_agent_sdk-0.1.48.tar.gz", hash = "sha256:ee294d3f02936c0b826119ffbefcf88c67731cf8c2d2cb7111ccc97f76344272", size = 87375, upload-time = "2026-03-07T00:21:37.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/cf/bbbdee52ee0c63c8709b0ac03ce3c1da5bdc37def5da0eca63363448744f/claude_agent_sdk-0.1.48-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5761ff1d362e0f17c2b1bfd890d1c897f0aa81091e37bbd15b7d06f05ced552d", size = 57559306, upload-time = "2026-03-07T00:21:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/57/d1/2179154b88d4cf6ba1cf6a15066ee8e96257aaeb1330e625e809ba2f28eb/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:39c1307daa17e42fa8a71180bb20af8a789d72d3891fc93519ff15540badcb83", size = 73980309, upload-time = "2026-03-07T00:21:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/dc/99/55b0cd3bf54a7449e744d23cf50be104e9445cf623e1ed75722112aa6264/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:543d70acba468eccfff836965a14b8ac88cf90809aeeb88431dfcea3ee9a2fa9", size = 74583686, upload-time = "2026-03-07T00:21:28.969Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/4851bd9a238b7aadba7639eb906aca7da32a51f01563fa4488469c608b3a/claude_agent_sdk-0.1.48-py3-none-win_amd64.whl", hash = "sha256:0d37e60bd2b17efc3f927dccef080f14897ab62cd1d0d67a4abc8a0e2d4f1006", size = 74956045, upload-time = "2026-03-07T00:21:33.475Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -766,6 +1256,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, ] +[[package]] +name = "clr-loader" +version = "0.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -891,6 +1393,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "durabletask" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncio" }, + { name = "grpcio" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/25/11d70b07723587a0b95fb57b5817627c9e605554b874697e5aeee3e5466d/durabletask-1.4.0.tar.gz", hash = "sha256:639138c10e2687a485ee94d218c27f8dc193376367dce9617f1ca2ec1cc8f021", size = 97252, upload-time = "2026-04-08T18:49:26.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3f/7250be7683aa6e9e89324db549e2b44cb6db7904cd315024933a23405e07/durabletask-1.4.0-py3-none-any.whl", hash = "sha256:75e11407bf24f045e32ef26b5e753f49f64fee822c8c9bfc5184a0911cb0969c", size = 107934, upload-time = "2026-04-08T18:49:24.856Z" }, +] + +[[package]] +name = "durabletask-azuremanaged" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "durabletask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/a9/18501dc091867a9bb5a7d184c69f3fac14294f34dea2363aa9379eeeedc3/durabletask_azuremanaged-1.4.0.tar.gz", hash = "sha256:739cde74ecdacf732fa4a9a40c0afba5d3185c5e575a6883d303c5a112f2c34a", size = 5657, upload-time = "2026-04-08T19:22:27.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/95/00ef2b2e0dd62fee6dc411aa1c4071ac55e54bcbd47a1384722e0ba54f42/durabletask_azuremanaged-1.4.0-py3-none-any.whl", hash = "sha256:80a0255afa7b61c01886d82dc22b75188b786f2454ea9f1a09dac10888a3c131", size = 7852, upload-time = "2026-04-08T19:22:26.624Z" }, +] + [[package]] name = "fastapi" version = "0.128.3" @@ -916,6 +1446,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] +[[package]] +name = "foundry-local-sdk" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/6b/76a7fe8f9f4c52cc84eaa1cd1b66acddf993496d55d6ea587bf0d0854d1c/foundry_local_sdk-0.5.1-py3-none-any.whl", hash = "sha256:f3639a3666bc3a94410004a91671338910ac2e1b8094b1587cc4db0f4a7df07e", size = 14003, upload-time = "2025-11-21T05:39:58.099Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1021,6 +1564,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "furl" +version = "2.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderedmultidict" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526, upload-time = "2025-03-09T05:36:21.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550, upload-time = "2025-03-09T05:36:19.928Z" }, +] + +[[package]] +name = "github-copilot-sdk" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/41/76a9d50d7600bf8d26c659dc113be62e4e56e00a5cbfd544e1b5b200f45c/github_copilot_sdk-0.2.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c0823150f3b73431f04caee43d1dbafac22ae7e8bd1fc83727ee8363089ee038", size = 61076141, upload-time = "2026-04-03T20:18:22.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/d2e8bf4587c4da270ccb9cbd5ab8a2c4b41217c2bf04a43904be8a27ae20/github_copilot_sdk-0.2.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ef7ff68eb8960515e1a2e199ac0ffb9a17cd3325266461e6edd7290e43dcf012", size = 57838464, upload-time = "2026-04-03T20:18:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/78/8b/cc8ee46724bd9fdfd6afe855a043c8403ed6884c5f3a55a9737780810396/github_copilot_sdk-0.2.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:890f7124e3b147532a1ac6c8d5f66421ea37757b2b9990d7967f3f147a2f533a", size = 63940155, upload-time = "2026-04-03T20:18:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ee/facf04e22e42d4bdd4fe3d356f3a51180a6ea769ae2ac306d0897f9bf9d9/github_copilot_sdk-0.2.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6502be0b9ececacbda671835e5f61c7aaa906c6b8657ee252cad6cc8335cac8e", size = 62130538, upload-time = "2026-04-03T20:18:34.061Z" }, + { url = "https://files.pythonhosted.org/packages/3f/1c/8b105f14bf61d1d304a00ac29460cb0d4e7406ceb89907d5a7b41a72fe85/github_copilot_sdk-0.2.1-py3-none-win_amd64.whl", hash = "sha256:8275ca8e387e6b29bc5155a3c02a0eb3d035c6bc7b1896253eb0d469f2385790", size = 56547331, upload-time = "2026-04-03T20:18:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/0ce319d2f618e9bc89f275e60b1920f4587eb0218bba6cbb84283dc7a7f3/github_copilot_sdk-0.2.1-py3-none-win_arm64.whl", hash = "sha256:1f9b59b7c41f31be416bf20818f58e25b6adc76f6d17357653fde6fbab662606", size = 54499549, upload-time = "2026-04-03T20:18:41.77Z" }, +] + [[package]] name = "google-adk" version = "1.14.1" @@ -1491,6 +2064,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, @@ -1499,6 +2073,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, @@ -1507,6 +2082,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, @@ -1515,6 +2091,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, @@ -1523,6 +2100,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, @@ -1641,6 +2219,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1666,6 +2266,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, ] +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1681,6 +2317,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + [[package]] name = "httpx-sse" version = "0.4.3" @@ -1690,6 +2331,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1835,6 +2485,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -1847,6 +2506,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, ] +[[package]] +name = "jsonpath-ng" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, +] + [[package]] name = "jsonpointer" version = "3.0.0" @@ -2220,9 +2888,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] -[package.optional-dependencies] -ws = [ - { name = "websockets" }, +[[package]] +name = "mem0ai" +version = "1.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai" }, + { name = "posthog" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pytz" }, + { name = "qdrant-client" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/1e/2f8a8cc4b8e7f6126f3367d27dc65eac5cd4ceb854888faa3a8f62a2c0a0/mem0ai-1.0.11.tar.gz", hash = "sha256:ddb803bedc22bd514606d262407782e88df929f6991b59f6972fb8a25cc06001", size = 201758, upload-time = "2026-04-06T11:31:43.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/b5/f822c94e1b901f8a700af134c2473646de9a7db26364566f6a72d527d235/mem0ai-1.0.11-py3-none-any.whl", hash = "sha256:bcf4d678dc0a4d4e8eccaebe05562eae022fcdc825a0e3095d02f28cf61a5b6d", size = 297138, upload-time = "2026-04-06T11:31:41.716Z" }, ] [[package]] @@ -2571,7 +3252,7 @@ provides-extras = ["dev"] name = "microsoft-agents-a365-tooling-extensions-agentframework" source = { editable = "libraries/microsoft-agents-a365-tooling-extensions-agentframework" } dependencies = [ - { name = "agent-framework-azure-ai" }, + { name = "agent-framework" }, { name = "azure-identity" }, { name = "httpx" }, { name = "microsoft-agents-a365-tooling" }, @@ -2594,7 +3275,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "agent-framework-azure-ai" }, + { name = "agent-framework" }, { name = "azure-identity" }, { name = "black", marker = "extra == 'dev'" }, { name = "httpx" }, @@ -2772,6 +3453,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/f3/64dc3bf13e46c6a09cc1983f66da2e42bf726586fe0f77f915977a6be7d8/microsoft_agents_activity-0.7.0-py3-none-any.whl", hash = "sha256:8d30a25dfd0f491b834be52b4a21ff90ab3b9360ec7e50770c050f1d4a39e5ce", size = 132592, upload-time = "2026-01-21T18:05:33.533Z" }, ] +[[package]] +name = "microsoft-agents-copilotstudio-client" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-agents-hosting-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/6e/6f3c6c2df7e6bc13b44eaca70696d29a76d18b884f125fc892ac2fb689b2/microsoft_agents_copilotstudio_client-0.7.0.tar.gz", hash = "sha256:2e6d7b8d2fccf313f6dffd3df17a21137730151c0557ad1ec08c6fb631a30d5f", size = 12636, upload-time = "2026-01-21T18:05:26.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/6d/023ea0254ccb3b97ee36540df2d87aa718912f70d6b6c529569fae675ca3/microsoft_agents_copilotstudio_client-0.7.0-py3-none-any.whl", hash = "sha256:a69947c49e782b552c5ede877277e73a86280aa2335a291f08fe3622ebfdabe9", size = 13425, upload-time = "2026-01-21T18:05:35.729Z" }, +] + [[package]] name = "microsoft-agents-hosting-core" version = "0.7.0" @@ -2788,6 +3481,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/e4/8d9e2e3f3a3106d0c80141631385206a6946f0b414cf863db851b98533e7/microsoft_agents_hosting_core-0.7.0-py3-none-any.whl", hash = "sha256:d03549fff01f38c1a96da4f79375c33378205ee9b5c6e01b87ba576f59b7887f", size = 133749, upload-time = "2026-01-21T18:05:38.002Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + [[package]] name = "mmh3" version = "5.2.0" @@ -3197,6 +3931,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] +[[package]] +name = "ollama" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/6d/ae96027416dcc2e98c944c050c492789502d7d7c0b95a740f0bb39268632/ollama-0.5.3.tar.gz", hash = "sha256:40b6dff729df3b24e56d4042fd9d37e231cee8e528677e0d085413a1d6692394", size = 43331, upload-time = "2025-08-07T21:44:10.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/f6/2091e50b8b6c3e6901f6eab283d5efd66fb71c86ddb1b4d68766c3eeba0f/ollama-0.5.3-py3-none-any.whl", hash = "sha256:a8303b413d99a9043dbf77ebf11ced672396b59bec27e6d5db67c88f01b279d2", size = 13490, upload-time = "2025-08-07T21:44:09.353Z" }, +] + [[package]] name = "openai" version = "2.28.0" @@ -3234,6 +3981,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/2e/23dbd9099555a9c7081c2819d00b7e1ee6ddbbd2fba8032f0ca4ddff778f/openai_agents-0.4.2-py3-none-any.whl", hash = "sha256:89fda02002dc0ac90ae177bb2f381a78b73aae329753bffb9276cfbdbfd20dc3", size = 216402, upload-time = "2025-10-24T21:46:32.065Z" }, ] +[[package]] +name = "openai-chatkit" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "openai" }, + { name = "openai-agents" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/46/b15fd77f7df12a2cabd8475de6226ce04d1cec7b283b21e8f0f52edc63a7/openai_chatkit-1.6.3.tar.gz", hash = "sha256:f16e347f39c376a78dddb5ceaf5398a4bb700c0145bfa7cb899d65135972956e", size = 61822, upload-time = "2026-03-04T19:30:19.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/5e/e06a4bec431083c282dea5729b0947b940900a4014216835182048078877/openai_chatkit-1.6.3-py3-none-any.whl", hash = "sha256:642ecdf810eda3619964f316e393f252741130a5500dc3a357d501f8657b3941", size = 42578, upload-time = "2026-03-04T19:30:18.314Z" }, +] + [[package]] name = "openapi-core" version = "0.19.4" @@ -3456,12 +4219,15 @@ wheels = [ ] [[package]] -name = "opentelemetry-semantic-conventions-ai" -version = "0.4.13" +name = "orderedmultidict" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368, upload-time = "2025-08-22T10:14:17.387Z" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973, upload-time = "2025-11-18T08:00:42.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080, upload-time = "2025-08-22T10:14:16.477Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897, upload-time = "2025-11-18T08:00:41.44Z" }, ] [[package]] @@ -3634,6 +4400,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "posthog" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/cc/cbebc13accaa015f733a67c74261ae13b86860cdf84e86a65e77c6125d8c/posthog-7.12.0.tar.gz", hash = "sha256:9385b207b87e642f17b784d74c18e6f28952ac7cde781d31675097d1a4dbc14e", size = 193445, upload-time = "2026-04-16T08:39:08.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/4b/40894963647d5044f591099cfe10fe16b688e8e6fb6ed980ccc2c784a3f5/posthog-7.12.0-py3-none-any.whl", hash = "sha256:df1444aa485b45318207f7b5100d955cde4d585e2e7f5b736ade45dcc6d2a60d", size = 226894, upload-time = "2026-04-16T08:39:05.896Z" }, +] + +[[package]] +name = "powerfx" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "pythonnet", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/96/0f8a1f86485b3ec0315e3e8403326884a0334b3dcd699df2482669cca4be/powerfx-0.0.34-py3-none-any.whl", hash = "sha256:f2dc1c42ba8bfa4c72a7fcff2a00755b95394547388ca0b3e36579c49ee7ed75", size = 3483089, upload-time = "2025-12-22T15:50:57.536Z" }, +] + [[package]] name = "prance" version = "25.4.8.0" @@ -4117,6 +4925,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pythonnet" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clr-loader", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506, upload-time = "2024-12-13T08:30:40.661Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -4151,6 +4980,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -4225,6 +5063,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "qdrant-client" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, +] + +[[package]] +name = "redis" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/80/2971931d27651affa88a44c0ad7b8c4a19dc29c998abb20b23868d319b59/redis-7.1.1.tar.gz", hash = "sha256:a2814b2bda15b39dad11391cc48edac4697214a8a5a4bd10abe936ab4892eb43", size = 4800064, upload-time = "2026-02-09T18:39:40.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/55/1de1d812ba1481fa4b37fb03b4eec0fcb71b6a0d44c04ea3482eb017600f/redis-7.1.1-py3-none-any.whl", hash = "sha256:f77817f16071c2950492c67d40b771fa493eb3fccc630a424a10976dbb794b7a", size = 356057, upload-time = "2026-02-09T18:39:38.602Z" }, +] + +[[package]] +name = "redisvl" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpath-ng" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "python-ulid" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/1a/f1f0ff963622c34a9e9a9f2a0c6ad82bfbd05c082ecc89e38e092e3e9069/redisvl-0.15.0.tar.gz", hash = "sha256:0e382e9b6cd8378dfe1515b18f92d125cfba905f6f3c5fe9b8904b3ca840d1ca", size = 861480, upload-time = "2026-02-27T14:02:33.366Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/23/5c5263a3cfc66957fa3bb154ef9441fbbcfb2f4eae910eb18e316db168b1/redisvl-0.15.0-py3-none-any.whl", hash = "sha256:aff716b9a9c4aef9c81de9a12d9939a0170ff3b3a1fe9d4164e94b131a754290", size = 197935, upload-time = "2026-02-27T14:02:31.262Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -4445,6 +5332,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + [[package]] name = "scipy" version = "1.17.0" @@ -4958,6 +5857,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + [[package]] name = "virtualenv" version = "21.2.0" @@ -5000,6 +5948,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + [[package]] name = "websockets" version = "15.0.1" From 9ef600ea835ecd1dd3c376df11e0e43e0e786907 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 16:45:01 +0530 Subject: [PATCH 13/19] 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 --- .../semantickernel/services/mcp_tool_registration_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py index 6d67a7ca..cb6539a0 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py @@ -176,9 +176,7 @@ async def add_tool_servers_to_agent( # Tools can be invoked because their underlying connections stay alive. self._connected_plugins.append(plugin) - self._logger.info( - f"Connected and added MCP plugin for: {server.mcp_server_name}" - ) + self._logger.info(f"Connected and added MCP plugin for: {server.mcp_server_name}") except Exception as e: self._logger.error(f"Failed to add tools from {server.mcp_server_name}: {str(e)}") From 3dd83fef9b325bbf6fc5eb1d4c6a209a8ca48692 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 16:50:42 +0530 Subject: [PATCH 14/19] 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 --- .../mcp_tool_server_configuration_service.py | 6 +++--- .../tooling/utils/utility.py | 10 +++++----- tests/tooling/test_mcp_server_configuration.py | 17 ++++++++--------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index 8204e879..b5c878d4 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -170,8 +170,8 @@ async def list_tool_servers( else: # Legacy call without auth context — guard against V2 servers. # V2 servers require per-audience OBO exchange; returning them without a - # token would cause silent 401s downstream. Raise early with a clear message - # so callers get an actionable migration hint (mirrors Node.js behaviour). + # token would cause silent 401s downstream. Raise early with a clear + # migration hint so callers know which parameters to add. v2_servers = [s for s in servers if self._is_v2_server(s)] if v2_servers: names = ", ".join( @@ -226,7 +226,7 @@ def _create_dev_token_acquirer(self) -> TokenAcquirer: The CLI (``a365 develop get-token``) writes tokens to the environment before the agent starts. Resolution order per server: - 1. ``BEARER_TOKEN_`` — per-server token (aligns with Node.js) + 1. ``BEARER_TOKEN_`` — per-server token 2. ``BEARER_TOKEN`` — shared fallback token Tokens are returned **without** a ``Bearer `` prefix. If the env var already contains diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py index 2ca8c700..91a8e295 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/utils/utility.py @@ -75,13 +75,13 @@ def is_development_environment() -> bool: Resolution order (first non-empty value wins): 1. ``PYTHON_ENVIRONMENT`` — explicit Python SDK variable used in current samples. 2. ``ENVIRONMENT`` — legacy Python SDK variable (backward compatibility). - 3. ``ASPNETCORE_ENVIRONMENT`` — .NET / Azure hosting convention. - 4. ``DOTNET_ENVIRONMENT`` — .NET generic-host convention. + 3. ``ASPNETCORE_ENVIRONMENT`` — Azure hosting convention. + 4. ``DOTNET_ENVIRONMENT`` — generic-host convention. 5. Defaults to ``"Development"`` when none of the above are set. - ``PYTHON_ENVIRONMENT`` and ``ENVIRONMENT`` are checked before the .NET variables - so legacy Python agents that set ``ENVIRONMENT=Production`` are not affected if an - unrelated process also sets ``ASPNETCORE_ENVIRONMENT`` (e.g. a .NET sidecar). + ``PYTHON_ENVIRONMENT`` and ``ENVIRONMENT`` are checked first so that agents + which explicitly set ``ENVIRONMENT=Production`` are not affected if a host + process also sets ``ASPNETCORE_ENVIRONMENT``. Returns: bool: True when the resolved environment is "development" (case-insensitive). diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 9ac738da..2e6c5a83 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -288,20 +288,19 @@ def test_v1_atg_audience_in_uri_form_falls_back_to_atg_scope(self): assert resolve_token_scope_for_server(server) == f"{self.PROD_ATG_APP_ID}/.default" # ------------------------------------------------------------------ - # V1 test-env — ATG_APP_ID overridden via MCP_PLATFORM_APP_ID + # V1 test-env — non-prod ATG audience falls through to V2 scope resolution # ------------------------------------------------------------------ def test_v1_test_env_shared_audience_not_treated_as_v2(self): - """V1 test env: test audience GUID is a different GUID from prod ATG — treated as V2 - unless the caller configures ATG_APP_ID to match. This test documents that the SDK - uses the prod ATG_APP_ID constant; test environments must set MCP_PLATFORM_APP_ID - via the CLI / .env so gateway returns the correct shared audience in discovery.""" + """Non-prod ATG audience GUID is different from the hardcoded prod ATG_APP_ID constant, + so it is classified as V2 and resolved to its own /.default scope. This is intentional: + the V2 token exchange works correctly for test app registrations because + Tools.ListInvoke.All is pre-consented. Use MCP_PLATFORM_AUTHENTICATION_SCOPE or + MCP_PLATFORM_ENDPOINT env vars to point the SDK at a non-prod gateway.""" from microsoft_agents_a365.tooling.utils.utility import resolve_token_scope_for_server - # test env audience is a different GUID — with prod ATG_APP_ID hardcoded, - # it is treated as V2 (resolved to its own /.default scope). - # This is intentional: V2 logic handles it correctly since Tools.ListInvoke.All - # is pre-consented on the test app registration. + # Non-prod ATG audience — classified as V2 because it does not match + # the hardcoded prod ATG_APP_ID constant. Resolved to its own /.default scope. server = self._make_server(audience=self.TEST_ATG_APP_ID, scope=None) assert resolve_token_scope_for_server(server) == f"{self.TEST_ATG_APP_ID}/.default" From 48e46838b6e6a02ca9f511bb93eda4037f03eb14 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 17:49:55 +0530 Subject: [PATCH 15/19] 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. --- .../extensions/agentframework/conftest.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/tooling/extensions/agentframework/conftest.py diff --git a/tests/tooling/extensions/agentframework/conftest.py b/tests/tooling/extensions/agentframework/conftest.py new file mode 100644 index 00000000..d75400e4 --- /dev/null +++ b/tests/tooling/extensions/agentframework/conftest.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Conftest for Agent Framework tooling extension unit tests. + +Ensures that ``agent_framework`` exports required by the production module are +available in the test environment. In some CI configurations the package is +importable but its internal initialisation leaves certain names absent from the +top-level namespace (e.g. a partial circular-import during collection). We +patch the stubs in at conftest *module load* time — which pytest guarantees +happens before it imports any test file in this directory tree. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +# fmt: off +import agent_framework as _af # noqa: E402 + +_REQUIRED = ("RawAgent", "MCPStreamableHTTPTool", "Message", "HistoryProvider") +for _name in _REQUIRED: + if not hasattr(_af, _name): + setattr(_af, _name, MagicMock(name=f"agent_framework.{_name}")) +# fmt: on From f2f5f5109f62f7aa28f6f2374b4b0ad3181cc37f Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 17:53:46 +0530 Subject: [PATCH 16/19] 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 " when callers already included the prefix. Align with the azureaifoundry extension by applying the same case-insensitive startswith guard before prepending the prefix. --- .../services/mcp_tool_registration_service.py | 6 +++++- .../extensions/openai/mcp_tool_registration_service.py | 6 +++++- .../services/mcp_tool_registration_service.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py index 3704e89d..d1b7f866 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py @@ -133,7 +133,11 @@ async def add_tool_servers_to_agent( # BEARER_TOKEN* env vars, or legacy V1 callers). if Constants.Headers.AUTHORIZATION not in server_headers and auth_token: server_headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + 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 diff --git a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py index 20db917f..149881e5 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py @@ -166,7 +166,11 @@ async def add_tool_servers_to_agent( # BEARER_TOKEN* env vars, or legacy V1 callers). if Constants.Headers.AUTHORIZATION not in server_headers and auth_token: server_headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + auth_token + if auth_token.lower().startswith( + f"{Constants.Headers.BEARER_PREFIX.lower()} " + ) + else f"{Constants.Headers.BEARER_PREFIX} {auth_token}" ) headers = { **base_headers, diff --git a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py index cb6539a0..42f9651c 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py @@ -146,7 +146,11 @@ async def add_tool_servers_to_agent( # BEARER_TOKEN* env vars, or legacy V1 callers). if Constants.Headers.AUTHORIZATION not in server_headers and auth_token: server_headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + 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 From 3fde2a2d4e9e56e272f47bc9758cc87414f05eeb Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 18:09:26 +0530 Subject: [PATCH 17/19] 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. --- .../services/mcp_tool_registration_service.py | 8 ++++++-- .../services/mcp_tool_registration_service.py | 10 +++++++--- .../services/test_mcp_tool_registration_service.py | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py index d1b7f866..f9c8d8b9 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py @@ -1,15 +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 List, Optional, Sequence +from typing import TYPE_CHECKING, List, Optional, Sequence from agent_framework import RawAgent, Message, HistoryProvider, MCPStreamableHTTPTool -from agent_framework.openai import OpenAIChatClient 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 diff --git a/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py index 04c56c13..b8f504a2 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py @@ -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: @@ -99,13 +102,14 @@ 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) + 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( diff --git a/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py index 0fe54221..22eab269 100644 --- a/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py +++ b/tests/tooling/extensions/azureaifoundry/services/test_mcp_tool_registration_service.py @@ -115,6 +115,7 @@ async def test_list_tool_servers_receives_auth_context( with ( patch(f"{_MODULE}.McpTool", return_value=mock_mcp_tool), + patch(f"{_MODULE}.is_development_environment", return_value=False), patch(f"{_MODULE}.Utility.resolve_agent_identity", return_value="test-aai"), patch(f"{_MODULE}.Utility.get_user_agent_header", return_value="AzureAIFoundry/1.0"), ): From 5e91a28a540ede7c585851f6728c9d5736088d77 Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 19:05:30 +0530 Subject: [PATCH 18/19] 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. --- .../mcp_tool_server_configuration_service.py | 23 +++- .../tooling/test_mcp_server_configuration.py | 121 ++++++++++++++++++ 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py index b5c878d4..5ee987f9 100644 --- a/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py +++ b/libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py @@ -40,6 +40,7 @@ ATG_APP_ID_URI, build_mcp_server_url, get_chat_history_endpoint, + get_mcp_platform_authentication_scope, get_tooling_gateway_for_digital_worker, is_development_environment, resolve_token_scope_for_server, @@ -233,23 +234,35 @@ def _create_dev_token_acquirer(self) -> TokenAcquirer: a ``Bearer `` prefix (any casing), it is stripped so that ``_attach_per_audience_tokens`` does not produce ``Authorization: Bearer Bearer …``. + A WARNING is emitted when the shared ``BEARER_TOKEN`` is used for a V2 server + whose resolved scope differs from the shared ATG scope, because the shared token + is audience-locked and will cause a 401 against that server's endpoint. + Returns: TokenAcquirer: Async callable ``(server, scope) → Optional[str]``. """ - shared_token = os.getenv("BEARER_TOKEN") + shared_scope = get_mcp_platform_authentication_scope()[0] - async def acquire(server: MCPServerConfig, _scope: str) -> Optional[str]: + async def acquire(server: MCPServerConfig, scope: str) -> Optional[str]: server_name = server.mcp_server_name or "" - per_server_token = os.getenv(f"BEARER_TOKEN_{server_name.upper()}") - token = per_server_token or shared_token + per_server_key = f"BEARER_TOKEN_{server_name.upper()}" + has_per_server = per_server_key in os.environ + token = os.environ.get(per_server_key) or os.environ.get("BEARER_TOKEN") if not token: return None + if token and not has_per_server and scope != shared_scope: + self._logger.warning( + f"Dev: MCP server '{server_name}' requires scope '{scope}' " + f"but only BEARER_TOKEN is set. The shared token is scoped to " + f"a different audience and will likely cause a 401. " + f"Set {per_server_key} to a token acquired for the correct audience." + ) # Strip an existing "Bearer " prefix (case-insensitive) so the caller # always receives a raw token and the Authorization header is never doubled. if token.lower().startswith("bearer "): token = token[7:] self._logger.debug( - f"Attached {'per-server' if per_server_token else 'shared'} " + f"Attached {'per-server' if has_per_server else 'shared'} " f"dev token for '{server.mcp_server_name}'" ) return token diff --git a/tests/tooling/test_mcp_server_configuration.py b/tests/tooling/test_mcp_server_configuration.py index 2e6c5a83..1c365799 100644 --- a/tests/tooling/test_mcp_server_configuration.py +++ b/tests/tooling/test_mcp_server_configuration.py @@ -244,6 +244,84 @@ async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_u assert servers[0].mcp_server_unique_name == "prod_server" assert servers[0].url == "https://prod.custom.url/mcp" + @patch.dict(os.environ, {"ENVIRONMENT": "Production"}) + @pytest.mark.asyncio + async def test_legacy_prod_path_raises_for_v2_server(self, service): + """Legacy call (no auth context) raises immediately when a V2 server is discovered. + + Callers that omit + authorization/auth_handler_name/turn_context must migrate to the full + TurnContext overload once V2 servers are present. + """ + v2_server_data = { + "mcpServers": [ + { + "mcpServerName": "V2Server", + "mcpServerUniqueName": "v2_server", + "url": "https://v2.example.com/mcp", + "audience": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + } + ] + } + + with patch("aiohttp.ClientSession") as mock_session_class: + mock_response = MagicMock() + mock_response.status = 200 + mock_response.json = AsyncMock(return_value=v2_server_data) + mock_response_cm = MagicMock() + mock_response_cm.__aenter__ = AsyncMock(return_value=mock_response) + mock_response_cm.__aexit__ = AsyncMock(return_value=None) + mock_session = MagicMock() + mock_session.get = MagicMock(return_value=mock_response_cm) + mock_session_cm = MagicMock() + mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_cm.__aexit__ = AsyncMock(return_value=None) + mock_session_class.return_value = mock_session_cm + + with pytest.raises(Exception, match="V2Server"): + await service.list_tool_servers( + agentic_app_id="test-app-id", + auth_token="test-token", + # No authorization / auth_handler_name / turn_context → legacy path + ) + + @patch.dict(os.environ, {"ENVIRONMENT": "Production"}) + @pytest.mark.asyncio + async def test_legacy_prod_path_ok_for_v1_only_servers(self, service): + """Legacy call succeeds when all discovered servers are V1 (no audience).""" + v1_server_data = { + "mcpServers": [ + { + "mcpServerName": "V1Server", + "mcpServerUniqueName": "v1_server", + "url": "https://v1.example.com/mcp", + } + ] + } + + with patch("aiohttp.ClientSession") as mock_session_class: + mock_response = MagicMock() + mock_response.status = 200 + mock_response.json = AsyncMock(return_value=v1_server_data) + mock_response_cm = MagicMock() + mock_response_cm.__aenter__ = AsyncMock(return_value=mock_response) + mock_response_cm.__aexit__ = AsyncMock(return_value=None) + mock_session = MagicMock() + mock_session.get = MagicMock(return_value=mock_response_cm) + mock_session_cm = MagicMock() + mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_cm.__aexit__ = AsyncMock(return_value=None) + mock_session_class.return_value = mock_session_cm + + servers = await service.list_tool_servers( + agentic_app_id="test-app-id", + auth_token="test-token", + # No authorization context — fine for V1-only + ) + + assert len(servers) == 1 + assert servers[0].mcp_server_name == "V1Server" + class TestResolveTokenScopeForServer: """Tests for resolve_token_scope_for_server() utility function.""" @@ -533,6 +611,49 @@ async def test_dev_acquirer_per_server_token_strips_bearer_prefix(self, service) assert result[0].headers["Authorization"] == "Bearer per-server-tok" + @pytest.mark.asyncio + async def test_dev_acquirer_warns_when_shared_token_used_for_v2_server(self, service): + """Warning emitted when BEARER_TOKEN is used for a V2 server with a different scope.""" + v2_server = self._make_server("MailV2", audience="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + with patch.dict( + os.environ, + {"BEARER_TOKEN": "shared-token"}, + clear=False, + ): + # Ensure the per-server key is absent so the fallback path is taken. + os.environ.pop("BEARER_TOKEN_MAILV2", None) + with patch.object(service, "_logger") as mock_logger: + acquire = service._create_dev_token_acquirer() + await service._attach_per_audience_tokens([v2_server], acquire) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + assert "BEARER_TOKEN_MAILV2" in warning_msg + assert "401" in warning_msg + + @pytest.mark.asyncio + async def test_dev_acquirer_no_warning_when_per_server_token_set(self, service): + """No warning when a per-server BEARER_TOKEN_ is present for a V2 server.""" + v2_server = self._make_server("MailV2", audience="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + with patch.dict( + os.environ, + {"BEARER_TOKEN": "shared-token", "BEARER_TOKEN_MAILV2": "per-server-token"}, + ): + with patch.object(service, "_logger") as mock_logger: + acquire = service._create_dev_token_acquirer() + await service._attach_per_audience_tokens([v2_server], acquire) + mock_logger.warning.assert_not_called() + + @pytest.mark.asyncio + async def test_dev_acquirer_no_warning_for_v1_server(self, service): + """No warning when shared BEARER_TOKEN is used for a V1 server (scope == shared scope).""" + v1_server = self._make_server("mail") # no audience → V1 → scope == shared_scope + with patch.dict(os.environ, {"BEARER_TOKEN": "shared-token"}): + os.environ.pop("BEARER_TOKEN_MAIL", None) + with patch.object(service, "_logger") as mock_logger: + acquire = service._create_dev_token_acquirer() + await service._attach_per_audience_tokens([v1_server], acquire) + mock_logger.warning.assert_not_called() + class TestPrepareGatewayHeaders: """Tests for _prepare_gateway_headers and _resolve_agent_id_for_header.""" From 6b64061b88af6e10ae342eff55b8912ebf18894a Mon Sep 17 00:00:00 2001 From: pmohapatra Date: Thu, 16 Apr 2026 19:14:23 +0530 Subject: [PATCH 19/19] Fix Bearer double-prefix in googleadk auth fallback googleadk unconditionally prepended 'Bearer' in the shared-token fallback path, producing 'Bearer Bearer ' when the caller already included the prefix. Apply the same guard used in the other three extensions. --- .../googleadk/services/mcp_tool_registration_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py index 60ee5c78..1eb8ab77 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py +++ b/libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py @@ -133,7 +133,11 @@ async def add_tool_servers_to_agent( # BEARER_TOKEN* env vars, or legacy V1 callers). if Constants.Headers.AUTHORIZATION not in server_headers and auth_token: server_headers[Constants.Headers.AUTHORIZATION] = ( - f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + 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