From b271047480e8648ead53f3f9fa8f985e56b21270 Mon Sep 17 00:00:00 2001 From: biswapm Date: Fri, 3 Apr 2026 19:08:16 +0530 Subject: [PATCH 01/10] feat(python): implement MCP V1/V2 per-audience token support across all Python samples - Update list_tool_servers() calls to pass authorization_context instead of auth_token - Extract V2 MCPServerConfig fields (audience, scope, publisher, headers) from SDK configs - Implement per-server header merging {**base_headers, **server_headers} in claude, crewai, google-adk - Update all ToolingManifest.json with V2 server catalog (mcp_Admin365_GraphTools, mcp_OneDriveRemoteServer, mcp_SharePointRemoteServer, mcp_TeamsServerV1) - Update .env.template files with AGENTIC_APP_ID and other V2-required variables - Add 59 unit tests across all 5 Python samples (pytest with SDK mocking) - Add a365 artifact patterns to .gitignore --- .gitignore | 9 + .../sample-agent/.env.template | 8 +- .../sample-agent/ToolingManifest.json | 34 +- .../sample-agent/pyproject.toml | 3 + .../sample-agent/tests/__init__.py | 2 + .../tests/test_tooling_manifest.py | 77 ++++ python/claude/sample-agent/.env.template | 3 + .../claude/sample-agent/ToolingManifest.json | 51 ++- .../mcp_tool_registration_service.py | 59 ++- python/claude/sample-agent/pyproject.toml | 4 + python/claude/sample-agent/tests/__init__.py | 2 + python/claude/sample-agent/tests/conftest.py | 43 ++ .../test_mcp_tool_registration_service.py | 376 ++++++++++++++++++ python/crewai/sample_agent/.env.template | 4 +- .../crewai/sample_agent/ToolingManifest.json | 51 ++- .../mcp_tool_registration_service.py | 59 ++- python/crewai/sample_agent/tests/__init__.py | 2 + python/crewai/sample_agent/tests/conftest.py | 41 ++ .../test_mcp_tool_registration_service.py | 266 +++++++++++++ python/google-adk/sample-agent/.env.template | 71 +++- .../sample-agent/ToolingManifest.json | 34 +- .../mcp_tool_registration_service.py | 30 +- .../google-adk/sample-agent/tests/__init__.py | 2 + .../google-adk/sample-agent/tests/conftest.py | 41 ++ .../test_mcp_tool_registration_service.py | 257 ++++++++++++ python/openai/sample-agent/.env.template | 79 +++- .../openai/sample-agent/ToolingManifest.json | 34 +- python/openai/sample-agent/tests/__init__.py | 2 + .../tests/test_tooling_manifest.py | 77 ++++ 29 files changed, 1589 insertions(+), 132 deletions(-) create mode 100644 python/agent-framework/sample-agent/tests/__init__.py create mode 100644 python/agent-framework/sample-agent/tests/test_tooling_manifest.py create mode 100644 python/claude/sample-agent/tests/__init__.py create mode 100644 python/claude/sample-agent/tests/conftest.py create mode 100644 python/claude/sample-agent/tests/test_mcp_tool_registration_service.py create mode 100644 python/crewai/sample_agent/tests/__init__.py create mode 100644 python/crewai/sample_agent/tests/conftest.py create mode 100644 python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py create mode 100644 python/google-adk/sample-agent/tests/__init__.py create mode 100644 python/google-adk/sample-agent/tests/conftest.py create mode 100644 python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py create mode 100644 python/openai/sample-agent/tests/__init__.py create mode 100644 python/openai/sample-agent/tests/test_tooling_manifest.py diff --git a/.gitignore b/.gitignore index 938dae64..f204a770 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,12 @@ coverage/ # OS-specific files .DS_Store Thumbs.db + +# Agent 365 generated config and deployment artifacts +a365.config.json +a365.generated.config.json +app.zip +app_logs.zip +app_logs/ +publish/ +manifest/ diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index b7cd2aaf..44f223ef 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -6,7 +6,13 @@ MCP_PLATFORM_ENDPOINT= # Authentication Handler Configuration # Set to "AGENTIC" for production agentic auth, or leave empty for no auth handler -AUTH_HANDLER_NAME= +AUTH_HANDLER_NAME=AGENTIC + +# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) +AGENTIC_APP_ID= + +# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) +AGENT_ID= # Logging LOG_LEVEL=INFO diff --git a/python/agent-framework/sample-agent/ToolingManifest.json b/python/agent-framework/sample-agent/ToolingManifest.json index 9d5cacf2..0fffc838 100644 --- a/python/agent-framework/sample-agent/ToolingManifest.json +++ b/python/agent-framework/sample-agent/ToolingManifest.json @@ -1,8 +1,36 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools", - "mcpServerUniqueName": "mcp_MailTools" + "mcpServerName": "mcp_Admin365_GraphTools", + "mcpServerUniqueName": "mcp_Admin365_GraphTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", + "scope": "McpServers.Admin365Graph.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_OneDriveRemoteServer", + "mcpServerUniqueName": "mcp_OneDriveRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_SharePointRemoteServer", + "mcpServerUniqueName": "mcp_SharePointRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_TeamsServerV1", + "mcpServerUniqueName": "mcp_TeamsServerV1", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", + "scope": "McpServers.Teams.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/agent-framework/sample-agent/pyproject.toml b/python/agent-framework/sample-agent/pyproject.toml index 97aaa010..e5fc0285 100644 --- a/python/agent-framework/sample-agent/pyproject.toml +++ b/python/agent-framework/sample-agent/pyproject.toml @@ -67,6 +67,9 @@ dev-dependencies = [ "mypy>=1.0.0", ] +[tool.pytest.ini_options] +testpaths = ["tests"] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/python/agent-framework/sample-agent/tests/__init__.py b/python/agent-framework/sample-agent/tests/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/python/agent-framework/sample-agent/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/python/agent-framework/sample-agent/tests/test_tooling_manifest.py b/python/agent-framework/sample-agent/tests/test_tooling_manifest.py new file mode 100644 index 00000000..46f1e303 --- /dev/null +++ b/python/agent-framework/sample-agent/tests/test_tooling_manifest.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Tests for agent-framework ToolingManifest.json structure. +Validates V2 MCP fields are present and correctly formed. +""" + +import json +import os +import pytest + +MANIFEST_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "ToolingManifest.json", +) + +MCP_SERVERS_ALL_PATTERN = "McpServers." +V2_AUDIENCE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + + +@pytest.fixture(scope="module") +def manifest(): + with open(MANIFEST_PATH) as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def servers(manifest): + return manifest["mcpServers"] + + +class TestManifestStructure: + def test_manifest_has_mcp_servers_key(self, manifest): + assert "mcpServers" in manifest + + def test_at_least_one_server(self, servers): + assert len(servers) > 0 + + def test_each_server_has_required_fields(self, servers): + required = {"mcpServerName", "mcpServerUniqueName", "url", "scope", "audience", "publisher"} + for s in servers: + missing = required - s.keys() + assert not missing, f"Server '{s.get('mcpServerName')}' missing fields: {missing}" + + def test_urls_are_https(self, servers): + for s in servers: + assert s["url"].startswith("https://"), f"Server '{s['mcpServerName']}' URL must be HTTPS" + + def test_urls_point_to_production_endpoint(self, servers): + for s in servers: + assert "agent365.svc.cloud.microsoft" in s["url"], ( + f"Server '{s['mcpServerName']}' should use production endpoint" + ) + + def test_no_null_scopes(self, servers): + for s in servers: + assert s["scope"] and s["scope"] != "null", ( + f"Server '{s['mcpServerName']}' has null/empty scope" + ) + + def test_mcp_servers_all_scopes_use_v2_audience(self, servers): + """Servers with McpServers.*.All scope must use the V2 audience GUID.""" + for s in servers: + if s["scope"].startswith(MCP_SERVERS_ALL_PATTERN): + assert s["audience"] == V2_AUDIENCE, ( + f"Server '{s['mcpServerName']}' with scope '{s['scope']}' " + f"must use audience '{V2_AUDIENCE}'" + ) + + def test_publisher_is_set(self, servers): + for s in servers: + assert s["publisher"], f"Server '{s['mcpServerName']}' has empty publisher" + + def test_no_duplicate_server_names(self, servers): + names = [s["mcpServerName"] for s in servers] + assert len(names) == len(set(names)), "Duplicate mcpServerName entries found" diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index a38cd33e..a1e3deb1 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -30,6 +30,9 @@ MCP_PLATFORM_ENDPOINT= # MICROSOFT 365 AGENTS SDK CONFIGURATION # ============================================================================= +# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) +AGENTIC_APP_ID= + # Agent ID (required for agentic authentication) AGENT_ID=your-agent-id diff --git a/python/claude/sample-agent/ToolingManifest.json b/python/claude/sample-agent/ToolingManifest.json index b8fe9815..0fffc838 100644 --- a/python/claude/sample-agent/ToolingManifest.json +++ b/python/claude/sample-agent/ToolingManifest.json @@ -1,39 +1,36 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools", - "mcpServerUniqueName": "mcp_MailTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", - "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_Admin365_GraphTools", + "mcpServerUniqueName": "mcp_Admin365_GraphTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", + "scope": "McpServers.Admin365Graph.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_M365Copilot", - "mcpServerUniqueName": "mcp_M365Copilot", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_M365Copilot", - "scope": "McpServers.CopilotMCP.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_OneDriveRemoteServer", + "mcpServerUniqueName": "mcp_OneDriveRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_CalendarTools", - "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", - "scope": "McpServers.Calendar.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_SharePointRemoteServer", + "mcpServerUniqueName": "mcp_SharePointRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_MeServer", - "mcpServerUniqueName": "mcp_MeServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MeServer", - "scope": "McpServers.Me.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - }, - { - "mcpServerName": "mcp_TeamsServer", - "mcpServerUniqueName": "mcp_TeamsServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer", + "mcpServerName": "mcp_TeamsServerV1", + "mcpServerUniqueName": "mcp_TeamsServerV1", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/claude/sample-agent/mcp_tool_registration_service.py b/python/claude/sample-agent/mcp_tool_registration_service.py index fb732b54..f47173dc 100644 --- a/python/claude/sample-agent/mcp_tool_registration_service.py +++ b/python/claude/sample-agent/mcp_tool_registration_service.py @@ -130,12 +130,17 @@ def _load_manifest_servers_fallback(self) -> List[Dict[str, Any]]: scope = server.get("scope", "") audience = server.get("audience", "") + publisher = server.get("publisher", "") + server_headers = server.get("headers", {}) + if url: servers.append({ "name": name, "url": url, "scope": scope, "audience": audience, + "publisher": publisher, + "headers": server_headers, }) self._logger.info(f" 📌 [Manifest] Server: {name} -> {url}") @@ -249,39 +254,57 @@ async def discover_and_connect_servers( mcp_server_configs = [] try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") + + # Build authorization context for V2 per-audience token acquisition + authorization_context = { + "auth": auth, + "auth_handler_name": auth_handler_name, + "context": context, + } + sdk_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, - auth_token=auth_token if auth_token else None, + authorization_context=authorization_context, ) - + # Convert SDK config objects to our format for config in sdk_configs: # Extract URL - try different attribute names the SDK might use server_url = getattr(config, "url", None) or \ getattr(config, "server_url", None) or \ getattr(config, "endpoint", None) - + server_name = getattr(config, "mcp_server_name", None) or \ getattr(config, "mcp_server_unique_name", None) or \ getattr(config, "name", "unknown") - + + # Extract V2 fields + audience = getattr(config, "audience", None) + scope = getattr(config, "scope", None) + publisher = getattr(config, "publisher", None) + server_headers = getattr(config, "headers", None) or {} + # If URL is not a full URL, it might just be the server name/path if not server_url: # Use server name as path if no URL provided server_url = getattr(config, "mcp_server_unique_name", None) or server_name - + # Build full URL full_url = self._build_full_url(server_url) - + if full_url: mcp_server_configs.append({ "name": server_name, "url": full_url, + "audience": audience, + "scope": scope, + "publisher": publisher, + "headers": server_headers, }) self._logger.info(f" 📌 [SDK] Server: {server_name} -> {full_url}") - + self._logger.info(f"📋 SDK discovered {len(mcp_server_configs)} MCP server(s)") - + except Exception as e: self._logger.warning(f"âš ī¸ McpToolServerConfigurationService failed: {e}") @@ -301,6 +324,7 @@ async def discover_and_connect_servers( name=server_config["name"], url=server_config["url"], auth_token=auth_token, + server_headers=server_config.get("headers", {}), ) if connection and connection.connected: @@ -330,36 +354,41 @@ async def _connect_to_server( name: str, url: str, auth_token: str, + server_headers: Optional[Dict[str, str]] = None, ) -> Optional[MCPServerConnection]: """ Connect to an MCP server and fetch its tools. - + Args: name: Server display name. url: Server URL endpoint. - auth_token: Authentication token. - + auth_token: Authentication token (V1 fallback). + server_headers: Per-server headers from SDK (V2 per-audience tokens). + Returns: MCPServerConnection with tools, or None if connection failed. """ # Check if this is a local server (no auth needed) is_local = url.startswith("http://localhost") or url.startswith("http://127.0.0.1") - + if is_local: - headers = { + base_headers = { "Content-Type": "application/json", } self._logger.info(f"🏠 Connecting to local MCP server: {url}") else: - if not auth_token: + if not auth_token and not server_headers: self._logger.warning(f"âš ī¸ Skipping remote server {name} - no auth token") return None - headers = { + base_headers = { Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", "User-Agent": f"Claude-Agent-SDK/1.0 ({self._orchestrator_name})", "Content-Type": "application/json", } self._logger.info(f"â˜ī¸ Connecting to remote MCP server: {url}") + + # V2: merge per-server headers (server_headers override base_headers) + headers = {**base_headers, **(server_headers or {})} connection = MCPServerConnection( name=name, diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index bc8c56b7..c80e0bb2 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -56,3 +56,7 @@ dev = [ "ruff>=0.1.0", "mypy>=1.0.0", ] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/python/claude/sample-agent/tests/__init__.py b/python/claude/sample-agent/tests/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/python/claude/sample-agent/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/python/claude/sample-agent/tests/conftest.py b/python/claude/sample-agent/tests/conftest.py new file mode 100644 index 00000000..9565eb84 --- /dev/null +++ b/python/claude/sample-agent/tests/conftest.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Pytest configuration: mock Microsoft/Google SDK imports that may not be installed +in the test environment so unit tests can import the service modules directly. +""" + +import sys +from unittest.mock import MagicMock + + +def _mock_sdk(): + mocks = { + "microsoft_agents": MagicMock(), + "microsoft_agents.hosting": MagicMock(), + "microsoft_agents.hosting.core": MagicMock(), + "microsoft_agents_a365": MagicMock(), + "microsoft_agents_a365.tooling": MagicMock(), + "microsoft_agents_a365.tooling.utils": MagicMock(), + "microsoft_agents_a365.tooling.utils.constants": MagicMock(), + "microsoft_agents_a365.tooling.utils.utility": MagicMock(), + "microsoft_agents_a365.tooling.services": MagicMock(), + "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), + } + + # Constants.Headers stubs + headers_mock = MagicMock() + headers_mock.AUTHORIZATION = "Authorization" + headers_mock.BEARER_PREFIX = "Bearer" + mocks["microsoft_agents_a365.tooling.utils.constants"].Constants.Headers = headers_mock + + # get_mcp_platform_authentication_scope stub + mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( + lambda: ["https://api.powerplatform.com/.default"] + ) + + for name, mock in mocks.items(): + if name not in sys.modules: + sys.modules[name] = mock + + +_mock_sdk() diff --git a/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py b/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py new file mode 100644 index 00000000..32ea8b41 --- /dev/null +++ b/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py @@ -0,0 +1,376 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Unit tests for Claude MCP Tool Registration Service. + +Covers V2 MCP changes: +- authorization_context passed to list_tool_servers +- publisher and headers extracted from SDK configs and ToolingManifest.json +- Per-server header merging: {**base_headers, **server_headers} +""" + +import json +import os +import sys +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from mcp_tool_registration_service import McpToolRegistrationService + + +# --------------------------------------------------------------------------- +# _build_full_url +# --------------------------------------------------------------------------- + +class TestBuildFullUrl: + def setup_method(self): + self.service = McpToolRegistrationService() + + def test_full_https_url_returned_unchanged(self): + url = "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test" + assert self.service._build_full_url(url) == url + + def test_full_http_url_returned_unchanged(self): + url = "http://localhost:8080/mcp" + assert self.service._build_full_url(url) == url + + def test_relative_agents_path_prepends_endpoint(self): + with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): + result = self.service._build_full_url("agents/servers/mcp_Test") + assert result == "https://my.endpoint.com/agents/servers/mcp_Test" + + def test_bare_server_name_becomes_agents_servers_path(self): + with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): + result = self.service._build_full_url("mcp_Test") + assert result == "https://my.endpoint.com/agents/servers/mcp_Test" + + def test_leading_slash_stripped(self): + with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): + result = self.service._build_full_url("/agents/servers/mcp_Test") + assert result == "https://my.endpoint.com/agents/servers/mcp_Test" + + def test_empty_string_returns_empty(self): + assert self.service._build_full_url("") == "" + + +# --------------------------------------------------------------------------- +# _load_manifest_servers_fallback +# --------------------------------------------------------------------------- + +class TestLoadManifestServersFallback: + def setup_method(self): + self.service = McpToolRegistrationService() + + def test_loads_all_v2_fields(self, tmp_path): + manifest = { + "mcpServers": [{ + "mcpServerName": "mcp_Test", + "mcpServerUniqueName": "mcp_Test", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test", + "scope": "McpServers.Test.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft", + "headers": {"X-Custom": "value"}, + }] + } + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + + assert len(servers) == 1 + s = servers[0] + assert s["name"] == "mcp_Test" + assert s["scope"] == "McpServers.Test.All" + assert s["audience"] == "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + assert s["publisher"] == "Microsoft" + assert s["headers"] == {"X-Custom": "value"} + + def test_skips_servers_without_url(self, tmp_path): + manifest = {"mcpServers": [{"mcpServerName": "mcp_NoUrl"}]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + + assert servers == [] + + def test_defaults_publisher_and_headers_when_absent(self, tmp_path): + manifest = {"mcpServers": [{ + "mcpServerName": "mcp_Min", + "url": "https://example.com/mcp_Min", + }]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + + assert servers[0]["publisher"] == "" + assert servers[0]["headers"] == {} + + def test_returns_empty_when_manifest_missing(self, tmp_path): + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + assert servers == [] + + def test_multiple_servers_loaded(self, tmp_path): + manifest = {"mcpServers": [ + {"mcpServerName": "mcp_A", "url": "https://example.com/A"}, + {"mcpServerName": "mcp_B", "url": "https://example.com/B"}, + ]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + + assert len(servers) == 2 + + +# --------------------------------------------------------------------------- +# _connect_to_server +# --------------------------------------------------------------------------- + +class TestConnectToServer: + def setup_method(self): + self.service = McpToolRegistrationService() + + @pytest.mark.asyncio + async def test_v2_server_headers_override_base_auth_token(self): + """Per-audience token in server_headers must override the base auth_token.""" + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token="base-token", + server_headers={"Authorization": "Bearer per-audience-token"}, + ) + + assert captured["headers"]["Authorization"] == "Bearer per-audience-token" + + @pytest.mark.asyncio + async def test_base_token_used_when_no_server_headers(self): + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token="base-token", + server_headers={}, + ) + + assert "Bearer base-token" in captured["headers"].get("Authorization", "") + + @pytest.mark.asyncio + async def test_additional_server_headers_merged(self): + """Extra custom headers from server_headers should appear in final headers.""" + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token="base-token", + server_headers={"X-Tenant": "tenant-123"}, + ) + + assert captured["headers"]["X-Tenant"] == "tenant-123" + assert "Bearer base-token" in captured["headers"].get("Authorization", "") + + @pytest.mark.asyncio + async def test_returns_none_for_remote_with_no_auth(self): + result = await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token=None, + server_headers={}, + ) + assert result is None + + @pytest.mark.asyncio + async def test_local_server_connects_without_token(self): + async def mock_list(url, headers, name): + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + result = await self.service._connect_to_server( + name="local", + url="http://localhost:9999/mcp", + auth_token=None, + server_headers={}, + ) + + assert result is not None + assert result.connected is True + + @pytest.mark.asyncio + async def test_returns_none_when_server_headers_have_auth_but_auth_token_none(self): + """V2: server_headers with Authorization should allow connection even if auth_token is None.""" + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + result = await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token=None, + server_headers={"Authorization": "Bearer per-audience-token"}, + ) + + # Should NOT return None since server_headers provides auth + assert result is not None + assert captured["headers"]["Authorization"] == "Bearer per-audience-token" + + +# --------------------------------------------------------------------------- +# discover_and_connect_servers — authorization_context & V2 field extraction +# --------------------------------------------------------------------------- + +class TestDiscoverAndConnectServers: + def setup_method(self): + self.service = McpToolRegistrationService() + + @pytest.mark.asyncio + async def test_passes_authorization_context_to_sdk(self): + mock_auth = MagicMock() + mock_context = MagicMock() + captured = {} + + async def mock_list(**kwargs): + captured.update(kwargs) + return [] + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert "authorization_context" in captured + ctx = captured["authorization_context"] + assert ctx["auth"] is mock_auth + assert ctx["auth_handler_name"] == "AGENTIC" + assert ctx["context"] is mock_context + + @pytest.mark.asyncio + async def test_extracts_v2_fields_from_sdk_configs(self): + mock_auth = MagicMock() + mock_context = MagicMock() + + cfg = MagicMock() + cfg.url = "https://example.com/servers/mcp_Test" + cfg.mcp_server_name = "mcp_Test" + cfg.mcp_server_unique_name = "mcp_Test" + cfg.audience = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + cfg.scope = "McpServers.Test.All" + cfg.publisher = "Microsoft" + cfg.headers = {"Authorization": "Bearer per-audience-token"} + + async def mock_list(**kwargs): + return [cfg] + + connect_calls = [] + + async def mock_connect(name, url, auth_token, server_headers=None): + connect_calls.append({"name": name, "server_headers": server_headers}) + conn = MagicMock() + conn.connected = True + conn.tools = [] + conn.url = url + return conn + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert len(connect_calls) == 1 + assert connect_calls[0]["server_headers"] == {"Authorization": "Bearer per-audience-token"} + + @pytest.mark.asyncio + async def test_falls_back_to_manifest_when_sdk_fails(self, tmp_path): + mock_auth = MagicMock() + mock_context = MagicMock() + + manifest = {"mcpServers": [{ + "mcpServerName": "mcp_Fallback", + "url": "http://localhost:9999/mcp", + "scope": "McpServers.Test.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft", + }]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + async def sdk_fails(**kwargs): + raise RuntimeError("SDK unavailable") + + async def mock_connect(name, url, auth_token, server_headers=None): + conn = MagicMock() + conn.connected = True + conn.tools = [] + conn.url = url + conn.name = name + conn.headers = server_headers or {} + return conn + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=sdk_fails): + with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): + with patch("os.getcwd", return_value=str(tmp_path)): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert len(self.service._connected_servers) == 1 + assert self.service._connected_servers[0].name == "mcp_Fallback" + + @pytest.mark.asyncio + async def test_agentic_app_id_passed_to_sdk(self): + mock_auth = MagicMock() + mock_context = MagicMock() + captured = {} + + async def mock_list(**kwargs): + captured.update(kwargs) + return [] + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): + await self.service.discover_and_connect_servers( + agentic_app_id="my-unique-app-id", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert captured.get("agentic_app_id") == "my-unique-app-id" diff --git a/python/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index a4faa8bb..c856a2fb 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -112,7 +112,9 @@ MCP_DEVELOPMENT_BASE_URL= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://graph.microsoft.com/.default +# V2 MCP: per-audience scopes are resolved automatically by the SDK via authorization_context. +# Set this to the Agent365 platform scope for the initial blueprint token exchange. +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default # Service URL mapping CONNECTIONSMAP__0__SERVICEURL=* diff --git a/python/crewai/sample_agent/ToolingManifest.json b/python/crewai/sample_agent/ToolingManifest.json index b8fe9815..0fffc838 100644 --- a/python/crewai/sample_agent/ToolingManifest.json +++ b/python/crewai/sample_agent/ToolingManifest.json @@ -1,39 +1,36 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools", - "mcpServerUniqueName": "mcp_MailTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", - "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_Admin365_GraphTools", + "mcpServerUniqueName": "mcp_Admin365_GraphTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", + "scope": "McpServers.Admin365Graph.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_M365Copilot", - "mcpServerUniqueName": "mcp_M365Copilot", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_M365Copilot", - "scope": "McpServers.CopilotMCP.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_OneDriveRemoteServer", + "mcpServerUniqueName": "mcp_OneDriveRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_CalendarTools", - "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", - "scope": "McpServers.Calendar.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_SharePointRemoteServer", + "mcpServerUniqueName": "mcp_SharePointRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_MeServer", - "mcpServerUniqueName": "mcp_MeServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MeServer", - "scope": "McpServers.Me.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - }, - { - "mcpServerName": "mcp_TeamsServer", - "mcpServerUniqueName": "mcp_TeamsServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer", + "mcpServerName": "mcp_TeamsServerV1", + "mcpServerUniqueName": "mcp_TeamsServerV1", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/crewai/sample_agent/mcp_tool_registration_service.py b/python/crewai/sample_agent/mcp_tool_registration_service.py index ebdbefe5..657f3739 100644 --- a/python/crewai/sample_agent/mcp_tool_registration_service.py +++ b/python/crewai/sample_agent/mcp_tool_registration_service.py @@ -126,12 +126,17 @@ def _load_manifest_servers_fallback(self) -> List[Dict[str, Any]]: scope = server.get("scope", "") audience = server.get("audience", "") + publisher = server.get("publisher", "") + server_headers = server.get("headers", {}) + if url: servers.append({ "name": name, "url": url, "scope": scope, "audience": audience, + "publisher": publisher, + "headers": server_headers, }) self._logger.info(f" 📌 [Manifest] Server: {name} -> {url}") @@ -234,39 +239,57 @@ async def discover_and_connect_servers( mcp_server_configs = [] try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") + + # Build authorization context for V2 per-audience token acquisition + authorization_context = { + "auth": auth, + "auth_handler_name": auth_handler_name, + "context": context, + } + sdk_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, - auth_token=auth_token if auth_token else None, + authorization_context=authorization_context, ) - + # Convert SDK config objects to our format for config in sdk_configs: # Extract URL - try different attribute names the SDK might use server_url = getattr(config, "url", None) or \ getattr(config, "server_url", None) or \ getattr(config, "endpoint", None) - + server_name = getattr(config, "mcp_server_name", None) or \ getattr(config, "mcp_server_unique_name", None) or \ getattr(config, "name", "unknown") - + + # Extract V2 fields + audience = getattr(config, "audience", None) + scope = getattr(config, "scope", None) + publisher = getattr(config, "publisher", None) + server_headers = getattr(config, "headers", None) or {} + # If URL is not a full URL, it might just be the server name/path if not server_url: # Use server name as path if no URL provided server_url = getattr(config, "mcp_server_unique_name", None) or server_name - + # Build full URL full_url = self._build_full_url(server_url) - + if full_url: mcp_server_configs.append({ "name": server_name, "url": full_url, + "audience": audience, + "scope": scope, + "publisher": publisher, + "headers": server_headers, }) self._logger.info(f" 📌 [SDK] Server: {server_name} -> {full_url}") - + self._logger.info(f"📋 SDK discovered {len(mcp_server_configs)} MCP server(s)") - + except Exception as e: self._logger.warning(f"âš ī¸ McpToolServerConfigurationService failed: {e}") @@ -286,6 +309,7 @@ async def discover_and_connect_servers( name=server_config["name"], url=server_config["url"], auth_token=auth_token, + server_headers=server_config.get("headers", {}), ) if connection and connection.connected: @@ -324,36 +348,41 @@ async def _connect_to_server( name: str, url: str, auth_token: str, + server_headers: Optional[Dict[str, str]] = None, ) -> Optional[MCPServerConnection]: """ Connect to an MCP server and fetch its tools. - + Args: name: Server display name. url: Server URL endpoint. - auth_token: Authentication token. - + auth_token: Authentication token (V1 fallback). + server_headers: Per-server headers from SDK (V2 per-audience tokens). + Returns: MCPServerConnection with tools, or None if connection failed. """ # Check if this is a local server (no auth needed) is_local = url.startswith("http://localhost") or url.startswith("http://127.0.0.1") - + if is_local: - headers = { + base_headers = { "Content-Type": "application/json", } self._logger.info(f"🏠 Connecting to local MCP server: {url}") else: - if not auth_token: + if not auth_token and not server_headers: self._logger.warning(f"âš ī¸ Skipping remote server {name} - no auth token") return None - headers = { + base_headers = { Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", "User-Agent": f"CrewAI-Agent-SDK/1.0 ({self._orchestrator_name})", "Content-Type": "application/json", } self._logger.info(f"â˜ī¸ Connecting to remote MCP server: {url}") + + # V2: merge per-server headers (server_headers override base_headers) + headers = {**base_headers, **(server_headers or {})} connection = MCPServerConnection( name=name, diff --git a/python/crewai/sample_agent/tests/__init__.py b/python/crewai/sample_agent/tests/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/python/crewai/sample_agent/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/python/crewai/sample_agent/tests/conftest.py b/python/crewai/sample_agent/tests/conftest.py new file mode 100644 index 00000000..80113008 --- /dev/null +++ b/python/crewai/sample_agent/tests/conftest.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Pytest configuration: mock Microsoft SDK imports that may not be installed +in the test environment so unit tests can import the service modules directly. +""" + +import sys +from unittest.mock import MagicMock + + +def _mock_sdk(): + mocks = { + "microsoft_agents": MagicMock(), + "microsoft_agents.hosting": MagicMock(), + "microsoft_agents.hosting.core": MagicMock(), + "microsoft_agents_a365": MagicMock(), + "microsoft_agents_a365.tooling": MagicMock(), + "microsoft_agents_a365.tooling.utils": MagicMock(), + "microsoft_agents_a365.tooling.utils.constants": MagicMock(), + "microsoft_agents_a365.tooling.utils.utility": MagicMock(), + "microsoft_agents_a365.tooling.services": MagicMock(), + "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), + } + + headers_mock = MagicMock() + headers_mock.AUTHORIZATION = "Authorization" + headers_mock.BEARER_PREFIX = "Bearer" + mocks["microsoft_agents_a365.tooling.utils.constants"].Constants.Headers = headers_mock + + mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( + lambda: ["https://api.powerplatform.com/.default"] + ) + + for name, mock in mocks.items(): + if name not in sys.modules: + sys.modules[name] = mock + + +_mock_sdk() diff --git a/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py b/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py new file mode 100644 index 00000000..0d6f2a1c --- /dev/null +++ b/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py @@ -0,0 +1,266 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Unit tests for CrewAI MCP Tool Registration Service. + +Covers V2 MCP changes: +- authorization_context passed to list_tool_servers +- publisher and headers extracted from SDK configs and ToolingManifest.json +- Per-server header merging: {**base_headers, **server_headers} +""" + +import json +import os +import sys +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from mcp_tool_registration_service import McpToolRegistrationService + + +# --------------------------------------------------------------------------- +# _build_full_url +# --------------------------------------------------------------------------- + +class TestBuildFullUrl: + def setup_method(self): + self.service = McpToolRegistrationService() + + def test_full_url_unchanged(self): + url = "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test" + assert self.service._build_full_url(url) == url + + def test_bare_server_name_becomes_agents_servers_path(self): + with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://ep.com"}): + assert self.service._build_full_url("mcp_Test") == "https://ep.com/agents/servers/mcp_Test" + + def test_empty_returns_empty(self): + assert self.service._build_full_url("") == "" + + +# --------------------------------------------------------------------------- +# _load_manifest_servers_fallback +# --------------------------------------------------------------------------- + +class TestLoadManifestServersFallback: + def setup_method(self): + self.service = McpToolRegistrationService() + + def test_loads_publisher_and_headers(self, tmp_path): + manifest = {"mcpServers": [{ + "mcpServerName": "mcp_Test", + "url": "https://example.com/mcp_Test", + "scope": "McpServers.Test.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft", + "headers": {"X-Custom": "val"}, + }]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + + assert servers[0]["publisher"] == "Microsoft" + assert servers[0]["headers"] == {"X-Custom": "val"} + + def test_skips_servers_without_url(self, tmp_path): + manifest = {"mcpServers": [{"mcpServerName": "mcp_NoUrl"}]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + with patch("os.getcwd", return_value=str(tmp_path)): + assert self.service._load_manifest_servers_fallback() == [] + + def test_returns_empty_when_no_manifest(self, tmp_path): + with patch("os.getcwd", return_value=str(tmp_path)): + assert self.service._load_manifest_servers_fallback() == [] + + def test_defaults_missing_v2_fields(self, tmp_path): + manifest = {"mcpServers": [{"mcpServerName": "mcp_Min", "url": "https://example.com/mcp_Min"}]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + with patch("os.getcwd", return_value=str(tmp_path)): + servers = self.service._load_manifest_servers_fallback() + assert servers[0]["publisher"] == "" + assert servers[0]["headers"] == {} + + +# --------------------------------------------------------------------------- +# _connect_to_server +# --------------------------------------------------------------------------- + +class TestConnectToServer: + def setup_method(self): + self.service = McpToolRegistrationService() + + @pytest.mark.asyncio + async def test_v2_server_headers_override_base_token(self): + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token="base-token", + server_headers={"Authorization": "Bearer per-audience-token"}, + ) + + assert captured["headers"]["Authorization"] == "Bearer per-audience-token" + + @pytest.mark.asyncio + async def test_base_token_used_without_server_headers(self): + captured = {} + + async def mock_list(url, headers, name): + captured["headers"] = dict(headers) + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token="base-token", + server_headers={}, + ) + + assert "Bearer base-token" in captured["headers"].get("Authorization", "") + + @pytest.mark.asyncio + async def test_remote_skipped_with_no_auth(self): + result = await self.service._connect_to_server( + name="mcp_Test", + url="https://example.com/server", + auth_token=None, + server_headers={}, + ) + assert result is None + + @pytest.mark.asyncio + async def test_local_server_no_auth_required(self): + async def mock_list(url, headers, name): + return [] + + with patch.object(self.service, "_list_server_tools", side_effect=mock_list): + result = await self.service._connect_to_server( + name="local", + url="http://127.0.0.1:8080/mcp", + auth_token=None, + server_headers={}, + ) + + assert result is not None + assert result.connected is True + + +# --------------------------------------------------------------------------- +# discover_and_connect_servers +# --------------------------------------------------------------------------- + +class TestDiscoverAndConnectServers: + def setup_method(self): + self.service = McpToolRegistrationService() + + @pytest.mark.asyncio + async def test_passes_authorization_context_to_sdk(self): + mock_auth = MagicMock() + mock_context = MagicMock() + captured = {} + + async def mock_list(**kwargs): + captured.update(kwargs) + return [] + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + ctx = captured.get("authorization_context", {}) + assert ctx.get("auth") is mock_auth + assert ctx.get("auth_handler_name") == "AGENTIC" + + @pytest.mark.asyncio + async def test_per_server_headers_passed_to_connect(self): + mock_auth = MagicMock() + mock_context = MagicMock() + + cfg = MagicMock() + cfg.url = "https://example.com/servers/mcp_Test" + cfg.mcp_server_name = "mcp_Test" + cfg.mcp_server_unique_name = "mcp_Test" + cfg.audience = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + cfg.scope = "McpServers.Test.All" + cfg.publisher = "Microsoft" + cfg.headers = {"Authorization": "Bearer audience-token"} + + async def mock_list(**kwargs): + return [cfg] + + calls = [] + + async def mock_connect(name, url, auth_token, server_headers=None): + calls.append(server_headers) + conn = MagicMock() + conn.connected = True + conn.tools = [] + conn.url = url + return conn + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert calls[0] == {"Authorization": "Bearer audience-token"} + + @pytest.mark.asyncio + async def test_falls_back_to_manifest_when_sdk_fails(self, tmp_path): + mock_auth = MagicMock() + mock_context = MagicMock() + + manifest = {"mcpServers": [{ + "mcpServerName": "mcp_Fallback", + "url": "http://localhost:9999/mcp", + "scope": "McpServers.Test.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft", + }]} + (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) + + async def sdk_fails(**kwargs): + raise RuntimeError("SDK unavailable") + + async def mock_connect(name, url, auth_token, server_headers=None): + conn = MagicMock() + conn.connected = True + conn.tools = [] + conn.url = url + conn.name = name + conn.headers = {} + return conn + + with patch.object(self.service._config_service, "list_tool_servers", side_effect=sdk_fails): + with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): + with patch("os.getcwd", return_value=str(tmp_path)): + await self.service.discover_and_connect_servers( + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="tok", + ) + + assert len(self.service._connected_servers) == 1 + assert self.service._connected_servers[0].name == "mcp_Fallback" diff --git a/python/google-adk/sample-agent/.env.template b/python/google-adk/sample-agent/.env.template index 80dd31c5..7dcf7a82 100644 --- a/python/google-adk/sample-agent/.env.template +++ b/python/google-adk/sample-agent/.env.template @@ -1,22 +1,77 @@ GOOGLE_GENAI_USE_VERTEXAI=FALSE GOOGLE_API_KEY= -# Agent365 Agentic Authentication Configuration +# ============================================================================= +# MCP (Model Context Protocol) CONFIGURATION +# ============================================================================= + +# MCP Platform Endpoint (optional, defaults to https://agent365.svc.cloud.microsoft) +MCP_PLATFORM_ENDPOINT= + +# Bearer token for local development/testing (USE_AGENTIC_AUTH must be false) +# Generate with: a365 develop get-token -o raw +BEARER_TOKEN= + +# Use agentic authentication for production (set to true for Teams/M365 deployment) +USE_AGENTIC_AUTH=false + +# Authentication handler name +AUTH_HANDLER_NAME=AGENTIC + +# Agentic authentication scope +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default + +# ============================================================================= +# AGENT IDENTITY +# ============================================================================= + +# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) +AGENTIC_APP_ID= + +# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) +AGENT_ID= + +# These values are expected to be in the activity's recipient field +AGENTIC_UPN= +AGENTIC_NAME= +AGENTIC_USER_ID= +AGENTIC_TENANT_ID= + +# ============================================================================= +# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION +# ============================================================================= + +# Service connection settings (required for production agentic auth) CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default +# V2 MCP: per-audience scopes are resolved automatically by the SDK via authorization_context. +# Set this to the Agent365 platform scope for the initial blueprint token exchange. +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default +# Agent application user authorization settings AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization +AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default +# Connections map configuration CONNECTIONSMAP__0__SERVICEURL=* CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION -# These values are expected to be in the activity's recipient field -AGENTIC_UPN= -AGENTIC_NAME= -AGENTIC_USER_ID= -AGENTIC_APP_ID= -AGENTIC_TENANT_ID= \ No newline at end of file +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= + +PORT=3978 +LOG_LEVEL=INFO + +# ============================================================================= +# OBSERVABILITY CONFIGURATION +# ============================================================================= + +ENABLE_OBSERVABILITY=true +ENABLE_A365_OBSERVABILITY_EXPORTER=false +OBSERVABILITY_SERVICE_NAME=google-adk-agent-sample +OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +PYTHON_ENVIRONMENT=development diff --git a/python/google-adk/sample-agent/ToolingManifest.json b/python/google-adk/sample-agent/ToolingManifest.json index 9d5cacf2..0fffc838 100644 --- a/python/google-adk/sample-agent/ToolingManifest.json +++ b/python/google-adk/sample-agent/ToolingManifest.json @@ -1,8 +1,36 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools", - "mcpServerUniqueName": "mcp_MailTools" + "mcpServerName": "mcp_Admin365_GraphTools", + "mcpServerUniqueName": "mcp_Admin365_GraphTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", + "scope": "McpServers.Admin365Graph.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_OneDriveRemoteServer", + "mcpServerUniqueName": "mcp_OneDriveRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_SharePointRemoteServer", + "mcpServerUniqueName": "mcp_SharePointRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_TeamsServerV1", + "mcpServerUniqueName": "mcp_TeamsServerV1", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", + "scope": "McpServers.Teams.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index ab167173..55159343 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -54,6 +54,14 @@ async def add_tool_servers_to_agent( New Agent instance with all MCP servers """ + # Build authorization context for V2 per-audience token acquisition + authorization_context = { + "auth": auth, + "auth_handler_name": auth_handler_name, + "context": context, + } + + # V1 fallback: exchange a shared token if no bearer token provided if not auth_token: scopes = get_mcp_platform_authentication_scope() auth_token_obj = await auth.exchange_token(context, scopes, auth_handler_name) @@ -61,22 +69,30 @@ async def add_tool_servers_to_agent( 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, - auth_token=auth_token - ) + agentic_app_id=agentic_app_id, + authorization_context=authorization_context, + ) self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations") - # Convert MCP server configs to MCPServerInfo objects - mcp_servers_info = [] - mcp_server_headers = { + # Base headers used as fallback when no per-server headers are provided (V1) + base_headers = { "Authorization": f"Bearer {auth_token}" } + # Convert MCP server configs to McpToolset objects + mcp_servers_info = [] + for server_config in mcp_server_configs: + # V2: merge per-server headers (server_config.headers override base_headers) + server_level_headers = getattr(server_config, "headers", None) or {} + mcp_server_headers = {**base_headers, **server_level_headers} + + server_url = getattr(server_config, "url", None) or server_config.mcp_server_unique_name + server_info = McpToolset( connection_params=StreamableHTTPConnectionParams( - url=server_config.mcp_server_unique_name, + url=server_url, headers=mcp_server_headers ) ) diff --git a/python/google-adk/sample-agent/tests/__init__.py b/python/google-adk/sample-agent/tests/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/python/google-adk/sample-agent/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/python/google-adk/sample-agent/tests/conftest.py b/python/google-adk/sample-agent/tests/conftest.py new file mode 100644 index 00000000..0eb63402 --- /dev/null +++ b/python/google-adk/sample-agent/tests/conftest.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Pytest configuration: mock Google ADK and Microsoft SDK imports that may not be installed +in the test environment so unit tests can import the service modules directly. +""" + +import sys +from unittest.mock import MagicMock + + +def _mock_sdk(): + mocks = { + "google": MagicMock(), + "google.adk": MagicMock(), + "google.adk.agents": MagicMock(), + "google.adk.tools": MagicMock(), + "google.adk.tools.mcp_tool": MagicMock(), + "google.adk.tools.mcp_tool.mcp_toolset": MagicMock(), + "microsoft_agents": MagicMock(), + "microsoft_agents.hosting": MagicMock(), + "microsoft_agents.hosting.core": MagicMock(), + "microsoft_agents_a365": MagicMock(), + "microsoft_agents_a365.tooling": MagicMock(), + "microsoft_agents_a365.tooling.utils": MagicMock(), + "microsoft_agents_a365.tooling.utils.utility": MagicMock(), + "microsoft_agents_a365.tooling.services": MagicMock(), + "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), + } + + mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( + lambda: ["https://api.powerplatform.com/.default"] + ) + + for name, mock in mocks.items(): + if name not in sys.modules: + sys.modules[name] = mock + + +_mock_sdk() diff --git a/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py b/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py new file mode 100644 index 00000000..2ff660c6 --- /dev/null +++ b/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py @@ -0,0 +1,257 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Unit tests for Google ADK MCP Tool Registration Service. + +Covers V2 MCP changes: +- authorization_context passed to list_tool_servers +- Per-server header merging: {**base_headers, **server_config.headers} +- Server URL resolved from server_config.url (V2) over mcp_server_unique_name (V1) +""" + +import os +import sys +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Patch McpToolset and StreamableHTTPConnectionParams before importing the module +import sys +from unittest.mock import MagicMock + +McpToolsetMock = MagicMock +StreamableHTTPConnectionParamsMock = MagicMock + +import mcp_tool_registration_service as svc_module +svc_module.McpToolset = McpToolsetMock +svc_module.StreamableHTTPConnectionParams = StreamableHTTPConnectionParamsMock + +from mcp_tool_registration_service import McpToolRegistrationService + + +class TestAddToolServersToAgent: + def setup_method(self): + self.service = McpToolRegistrationService() + + def _make_agent(self, tools=None): + agent = MagicMock() + agent.name = "test-agent" + agent.model = "gemini-pro" + agent.description = "Test agent" + agent.tools = tools or [] + return agent + + def _make_server_config(self, name="mcp_Test", url=None, headers=None): + cfg = MagicMock() + cfg.mcp_server_unique_name = name + cfg.url = url + cfg.headers = headers or {} + return cfg + + @pytest.mark.asyncio + async def test_passes_authorization_context_to_sdk(self): + mock_auth = MagicMock() + mock_context = MagicMock() + token_result = MagicMock() + token_result.token = "exchange-token" + mock_auth.exchange_token = AsyncMock(return_value=token_result) + captured = {} + + async def mock_list(**kwargs): + captured.update(kwargs) + return [] + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + await self.service.add_tool_servers_to_agent( + agent=self._make_agent(), + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + ) + + assert "authorization_context" in captured + ctx = captured["authorization_context"] + assert ctx["auth"] is mock_auth + assert ctx["auth_handler_name"] == "AGENTIC" + assert ctx["context"] is mock_context + + @pytest.mark.asyncio + async def test_per_server_headers_override_base_headers(self): + mock_auth = MagicMock() + mock_context = MagicMock() + token_result = MagicMock() + token_result.token = "base-token" + mock_auth.exchange_token = AsyncMock(return_value=token_result) + + cfg = self._make_server_config( + url="https://example.com/servers/mcp_Test", + headers={"Authorization": "Bearer per-audience-token", "X-Custom": "val"}, + ) + + async def mock_list(**kwargs): + return [cfg] + + captured_params = [] + + def mock_toolset(connection_params): + captured_params.append(connection_params) + return MagicMock() + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): + # Capture StreamableHTTPConnectionParams calls + param_calls = [] + + def mock_params(url, headers): + param_calls.append({"url": url, "headers": headers}) + return MagicMock() + + with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): + await self.service.add_tool_servers_to_agent( + agent=self._make_agent(), + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + ) + + assert len(param_calls) == 1 + headers = param_calls[0]["headers"] + # Per-audience token overrides base token + assert headers["Authorization"] == "Bearer per-audience-token" + assert headers["X-Custom"] == "val" + + @pytest.mark.asyncio + async def test_base_token_used_when_no_server_headers(self): + mock_auth = MagicMock() + mock_context = MagicMock() + token_result = MagicMock() + token_result.token = "base-token" + mock_auth.exchange_token = AsyncMock(return_value=token_result) + + cfg = self._make_server_config(url="https://example.com/servers/mcp_Test", headers={}) + + async def mock_list(**kwargs): + return [cfg] + + param_calls = [] + + def mock_params(url, headers): + param_calls.append({"url": url, "headers": headers}) + return MagicMock() + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): + with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): + await self.service.add_tool_servers_to_agent( + agent=self._make_agent(), + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + ) + + assert "Bearer base-token" in param_calls[0]["headers"]["Authorization"] + + @pytest.mark.asyncio + async def test_uses_server_config_url_over_unique_name(self): + """V2: server_config.url takes priority over mcp_server_unique_name.""" + mock_auth = MagicMock() + mock_context = MagicMock() + token_result = MagicMock() + token_result.token = "tok" + mock_auth.exchange_token = AsyncMock(return_value=token_result) + + cfg = self._make_server_config( + name="mcp_Test", + url="https://full-v2-url.example.com/servers/mcp_Test", + ) + + async def mock_list(**kwargs): + return [cfg] + + param_calls = [] + + def mock_params(url, headers): + param_calls.append({"url": url}) + return MagicMock() + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): + with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): + await self.service.add_tool_servers_to_agent( + agent=self._make_agent(), + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + ) + + assert param_calls[0]["url"] == "https://full-v2-url.example.com/servers/mcp_Test" + + @pytest.mark.asyncio + async def test_skips_token_exchange_when_auth_token_provided(self): + mock_auth = MagicMock() + mock_context = MagicMock() + mock_auth.exchange_token = AsyncMock() + + async def mock_list(**kwargs): + return [] + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + await self.service.add_tool_servers_to_agent( + agent=self._make_agent(), + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + auth_token="pre-provided", + ) + + mock_auth.exchange_token.assert_not_called() + + @pytest.mark.asyncio + async def test_returns_agent_with_mcp_tools_appended(self): + mock_auth = MagicMock() + mock_context = MagicMock() + token_result = MagicMock() + token_result.token = "tok" + mock_auth.exchange_token = AsyncMock(return_value=token_result) + + cfgs = [ + self._make_server_config(name="mcp_A", url="https://example.com/A"), + self._make_server_config(name="mcp_B", url="https://example.com/B"), + ] + + async def mock_list(**kwargs): + return cfgs + + fake_tool = MagicMock() + existing_tool = MagicMock() + agent = self._make_agent(tools=[existing_tool]) + + agent_ctor_calls = [] + + def mock_agent_ctor(**kwargs): + agent_ctor_calls.append(kwargs) + return MagicMock() + + with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): + with patch.object(svc_module, "McpToolset", return_value=fake_tool): + with patch.object(svc_module, "StreamableHTTPConnectionParams", return_value=MagicMock()): + with patch.object(svc_module, "Agent", side_effect=mock_agent_ctor): + await self.service.add_tool_servers_to_agent( + agent=agent, + agentic_app_id="test-app", + auth=mock_auth, + auth_handler_name="AGENTIC", + context=mock_context, + ) + + # Agent constructor called with original tool + 2 MCP toolsets + assert len(agent_ctor_calls) == 1 + tools_passed = agent_ctor_calls[0]["tools"] + assert len(tools_passed) == 3 # 1 existing + 2 MCP diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index c095efb8..e85629fa 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -1,47 +1,82 @@ -# This is a demo .env file -# Replace with your actual OpenAI API key +# ============================================================================= +# OPENAI / AZURE OPENAI CONFIGURATION +# ============================================================================= + +# Standard OpenAI API key +# Get your API key from https://platform.openai.com/api-keys OPENAI_API_KEY= +# OpenAI model to use +OPENAI_MODEL=gpt-4o-mini + +# Azure OpenAI Configuration (use instead of OPENAI_API_KEY for Azure deployments) +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini + +# ============================================================================= +# MCP (Model Context Protocol) CONFIGURATION +# ============================================================================= + # MCP Server Configuration MCP_SERVER_PORT=8000 MCP_SERVER_HOST=localhost MCP_DEVELOPMENT_BASE_URL= -# Logging -LOG_LEVEL=INFO +# Bearer token for local development/testing (USE_AGENTIC_AUTH must be false) +# Generate with: a365 develop get-token -o raw +BEARER_TOKEN= -# Observability Configuration -OBSERVABILITY_SERVICE_NAME=openai-agent-sample -OBSERVABILITY_SERVICE_NAMESPACE=agents.samples +# Use agentic authentication for production (set to true for Teams/M365 deployment) +USE_AGENTIC_AUTH=false -BEARER_TOKEN= -OPENAI_MODEL=gpt-4o-mini +# Authentication handler name +AUTH_HANDLER_NAME=AGENTIC + +# Agentic authentication scope +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default + +# ============================================================================= +# AGENT IDENTITY +# ============================================================================= -USE_AGENTIC_AUTH= +# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) +AGENTIC_APP_ID= +# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) AGENT_ID= -# Agent 365 Agentic Authentication Configuration +# ============================================================================= +# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION +# ============================================================================= + +# Service connection settings (required for production agentic auth) CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= - +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://graph.microsoft.com/.default + +# Agent application user authorization settings AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization -AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=AGENTBLUEPRINT +AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default - + CONNECTIONSMAP__0__SERVICEURL=* CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION -# Optional: Server Configuration +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= + PORT=3978 +LOG_LEVEL=INFO -# Azure OpenAI Configuration -AZURE_OPENAI_API_KEY= -AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_DEPLOYMENT="gpt-4o-mini" +# ============================================================================= +# OBSERVABILITY CONFIGURATION +# ============================================================================= -# Required for observability SDK ENABLE_OBSERVABILITY=true -ENABLE_KAIRO_EXPORTER=true -PYTHON_ENVIRONMENT=production +ENABLE_A365_OBSERVABILITY_EXPORTER=false +OBSERVABILITY_SERVICE_NAME=openai-agent-sample +OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +PYTHON_ENVIRONMENT=development diff --git a/python/openai/sample-agent/ToolingManifest.json b/python/openai/sample-agent/ToolingManifest.json index 9d5cacf2..0fffc838 100644 --- a/python/openai/sample-agent/ToolingManifest.json +++ b/python/openai/sample-agent/ToolingManifest.json @@ -1,8 +1,36 @@ { "mcpServers": [ { - "mcpServerName": "mcp_MailTools", - "mcpServerUniqueName": "mcp_MailTools" + "mcpServerName": "mcp_Admin365_GraphTools", + "mcpServerUniqueName": "mcp_Admin365_GraphTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", + "scope": "McpServers.Admin365Graph.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_OneDriveRemoteServer", + "mcpServerUniqueName": "mcp_OneDriveRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_SharePointRemoteServer", + "mcpServerUniqueName": "mcp_SharePointRemoteServer", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "scope": "Tools.ListInvoke.All", + "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", + "publisher": "Microsoft" + }, + { + "mcpServerName": "mcp_TeamsServerV1", + "mcpServerUniqueName": "mcp_TeamsServerV1", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", + "scope": "McpServers.Teams.All", + "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/openai/sample-agent/tests/__init__.py b/python/openai/sample-agent/tests/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/python/openai/sample-agent/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/python/openai/sample-agent/tests/test_tooling_manifest.py b/python/openai/sample-agent/tests/test_tooling_manifest.py new file mode 100644 index 00000000..f8995b34 --- /dev/null +++ b/python/openai/sample-agent/tests/test_tooling_manifest.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Tests for openai ToolingManifest.json structure. +Validates V2 MCP fields are present and correctly formed. +""" + +import json +import os +import pytest + +MANIFEST_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "ToolingManifest.json", +) + +MCP_SERVERS_ALL_PATTERN = "McpServers." +V2_AUDIENCE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + + +@pytest.fixture(scope="module") +def manifest(): + with open(MANIFEST_PATH) as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def servers(manifest): + return manifest["mcpServers"] + + +class TestManifestStructure: + def test_manifest_has_mcp_servers_key(self, manifest): + assert "mcpServers" in manifest + + def test_at_least_one_server(self, servers): + assert len(servers) > 0 + + def test_each_server_has_required_fields(self, servers): + required = {"mcpServerName", "mcpServerUniqueName", "url", "scope", "audience", "publisher"} + for s in servers: + missing = required - s.keys() + assert not missing, f"Server '{s.get('mcpServerName')}' missing fields: {missing}" + + def test_urls_are_https(self, servers): + for s in servers: + assert s["url"].startswith("https://"), f"Server '{s['mcpServerName']}' URL must be HTTPS" + + def test_urls_point_to_production_endpoint(self, servers): + for s in servers: + assert "agent365.svc.cloud.microsoft" in s["url"], ( + f"Server '{s['mcpServerName']}' should use production endpoint" + ) + + def test_no_null_scopes(self, servers): + for s in servers: + assert s["scope"] and s["scope"] != "null", ( + f"Server '{s['mcpServerName']}' has null/empty scope" + ) + + def test_mcp_servers_all_scopes_use_v2_audience(self, servers): + """Servers with McpServers.*.All scope must use the V2 audience GUID.""" + for s in servers: + if s["scope"].startswith(MCP_SERVERS_ALL_PATTERN): + assert s["audience"] == V2_AUDIENCE, ( + f"Server '{s['mcpServerName']}' with scope '{s['scope']}' " + f"must use audience '{V2_AUDIENCE}'" + ) + + def test_publisher_is_set(self, servers): + for s in servers: + assert s["publisher"], f"Server '{s['mcpServerName']}' has empty publisher" + + def test_no_duplicate_server_names(self, servers): + names = [s["mcpServerName"] for s in servers] + assert len(names) == len(set(names)), "Duplicate mcpServerName entries found" From 305ebbd6bfbff055e7c4dca229af1a90a0789cbf Mon Sep 17 00:00:00 2001 From: biswapm Date: Fri, 3 Apr 2026 19:53:15 +0530 Subject: [PATCH 02/10] updated --- .../sample-agent/.env.template | 135 +++++++++--- python/claude/sample-agent/.env.template | 144 ++++++------ python/crewai/sample_agent/.env.template | 205 ++++++++++-------- python/google-adk/sample-agent/.env.template | 114 +++++++--- python/openai/sample-agent/.env.template | 108 ++++++--- 5 files changed, 454 insertions(+), 252 deletions(-) diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index 44f223ef..bd4e9d62 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -1,58 +1,129 @@ -# This is a demo .env file -# Replace with your actual OpenAI API key -OPENAI_API_KEY= -MCP_SERVER_HOST= -MCP_PLATFORM_ENDPOINT= +# ============================================================================= +# Agent Framework Agent Sample — Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in ALL required values before running. +# Lines starting with # are comments. Never commit .env to source control. +# ============================================================================= -# Authentication Handler Configuration -# Set to "AGENTIC" for production agentic auth, or leave empty for no auth handler -AUTH_HANDLER_NAME=AGENTIC +# ----------------------------------------------------------------------------- +# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one) +# ----------------------------------------------------------------------------- -# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) -AGENTIC_APP_ID= +# --- Option A: Standard OpenAI --- +# Get your API key from https://platform.openai.com/api-keys +OPENAI_API_KEY=<> -# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) -AGENT_ID= +# OpenAI model to use (e.g. gpt-4o, gpt-4o-mini) +OPENAI_MODEL=gpt-4o-mini -# Logging -LOG_LEVEL=INFO +# --- Option B: Azure OpenAI (recommended for enterprise) --- +# Get these from: Azure Portal > Your Azure OpenAI resource > Keys and Endpoint +AZURE_OPENAI_API_KEY=<> +AZURE_OPENAI_ENDPOINT=<> # e.g. https://my-resource.openai.azure.com/ +AZURE_OPENAI_DEPLOYMENT=<> # e.g. gpt-4o or gpt-4o-mini +AZURE_OPENAI_API_VERSION=2025-01-01-preview -# Observability Configuration -OBSERVABILITY_SERVICE_NAME=agent-framework-sample -OBSERVABILITY_SERVICE_NAMESPACE=agent-framework.samples +# ============================================================================= +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) +# ============================================================================= + +# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID +AGENTIC_APP_ID=<> + +# Agent ID — used for observability tracing and as fallback identifier. +# Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-agent-framework-agent" +AGENT_ID=<> -BEARER_TOKEN= -OPENAI_MODEL= +# ============================================================================= +# AUTHENTICATION +# ============================================================================= +# --- Option A: Bearer Token (development / local testing) --- +# Generate with: a365 develop get-token -o raw +# This token expires — regenerate when you see 401 / MCP connection errors. +# When BEARER_TOKEN is set, agentic auth is bypassed. +BEARER_TOKEN=<> + +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=true -# Agent 365 Agentic Authentication Configuration -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES= +# Name of the auth handler configured in your app registration (default: AGENTIC) +AUTH_HANDLER_NAME=AGENTIC + +# ============================================================================= +# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED +# ============================================================================= + +# Hostname for a locally running custom MCP server (leave blank if not using one) +MCP_SERVER_HOST= + +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# These map to appsettings-style connection configuration loaded by the SDK. +# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets + +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> + +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> + +# OAuth scope(s) for the service connection token. +# Keep this as the Agent 365 platform scope for the blueprint token exchange. +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default + +# Agentic user authorization handler — controls how the SDK exchanges tokens for MCP tools. +# ALT_BLUEPRINT_NAME: name of the connection used as the token exchange blueprint AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization +AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION +# Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default +# Maps incoming Bot Framework service URLs to a connection name. +# Use * to match any service URL (recommended for most deployments). CONNECTIONSMAP_0_SERVICEURL=* CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION -# Optional: Server Configuration +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= + +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) PORT=3978 -# Azure OpenAI Configuration -AZURE_OPENAI_API_KEY= -AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_DEPLOYMENT= -AZURE_OPENAI_API_VERSION= +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL +LOG_LEVEL=INFO + +# ============================================================================= +# OBSERVABILITY (Agent 365 Telemetry) +# ============================================================================= -# Required for observability SDK +# Logical service name shown in traces / dashboards +OBSERVABILITY_SERVICE_NAME=agent-framework-sample + +# Namespace grouping for this sample in telemetry backends +OBSERVABILITY_SERVICE_NAMESPACE=agent-framework.samples + +# Master switch — enable OpenTelemetry tracing for this agent ENABLE_OBSERVABILITY=true + +# Set to "true" to ship traces to the Agent 365 cloud observability backend. +# Requires a valid token and is intended for production / staging deployments. ENABLE_A365_OBSERVABILITY_EXPORTER=false + +# Python environment label — influences routing and cluster selection in Agent 365 +# Options: development | production PYTHON_ENVIRONMENT=development -# Enable otel logs on AgentFramework SDK. Required for auto instrumentation +# Enable OpenTelemetry logs on the Agent Framework SDK (required for auto-instrumentation) ENABLE_OTEL=true + +# Set to "true" to include request/response payloads in traces (CAUTION: may log PII) ENABLE_SENSITIVE_DATA=true diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index a1e3deb1..ee029ed7 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -1,133 +1,151 @@ # ============================================================================= # CLAUDE AGENT SDK CONFIGURATION # ============================================================================= - -# Anthropic API Key (required) -# Get your API key from: https://console.anthropic.com/ -ANTHROPIC_API_KEY= - -# Claude Model to use (optional, defaults to claude-sonnet-4-20250514) -# Options: claude-opus-4-20250514, claude-sonnet-4-20250514, claude-haiku-4-20250514 -CLAUDE_MODEL=claude-sonnet-4-20250514 - - -# ============================================================================= -# MCP (Model Context Protocol) CONFIGURATION (Optional) +# Copy this file to .env and fill in ALL required values before running. +# Lines starting with # are comments. Never commit .env to source control. # ============================================================================= -# Environment label for MCP tooling (informational only) -# NOTE: The current runtime does NOT read ENVIRONMENT to control MCP discovery. -# MCP servers are discovered based on the MCP SDK behavior, not this setting. -ENVIRONMENT=Development +# ----------------------------------------------------------------------------- +# ANTHROPIC / CLAUDE API (REQUIRED) +# ----------------------------------------------------------------------------- -# MCP Server Host -MCP_SERVER_HOST= +# Your Anthropic API key — get it from https://console.anthropic.com/ +ANTHROPIC_API_KEY=<> -# MCP Platform Endpoint -MCP_PLATFORM_ENDPOINT= +# Claude model to use (optional, defaults to claude-sonnet-4-20250514) +# Options: claude-opus-4-20250514 | claude-sonnet-4-20250514 | claude-haiku-4-20250514 +CLAUDE_MODEL=claude-sonnet-4-20250514 # ============================================================================= -# MICROSOFT 365 AGENTS SDK CONFIGURATION +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) # ============================================================================= -# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) -AGENTIC_APP_ID= +# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID +AGENTIC_APP_ID=<> -# Agent ID (required for agentic authentication) -AGENT_ID=your-agent-id +# Agent ID — used for observability tracing and as fallback identifier. +# Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-claude-agent" +AGENT_ID=<> -# Environment ID (optional, defaults to prod) -# Options: dev, test, preprod, prod +# Environment ID for MCP platform routing (optional, defaults to prod) +# Options: dev | test | preprod | prod ENVIRONMENT_ID=prod # ============================================================================= -# AUTHENTICATION OPTIONS +# AUTHENTICATION # ============================================================================= -# Use agentic authentication (optional, defaults to false) -# Set to "true" to use agentic authentication with M365 Agents SDK +# --- Option A: Bearer Token (development / local testing) --- +# Generate with: a365 develop get-token -o raw +# This token expires — regenerate when you see 401 / MCP connection errors. +# When BEARER_TOKEN is set, agentic auth is bypassed. +BEARER_TOKEN=<> + +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=true +# Name of the auth handler configured in your app registration (default: AGENTIC) AUTH_HANDLER_NAME=AGENTIC -# Bearer token (required if not using client credentials) -# Use for development/testing without full app registration -BEARER_TOKEN= - -# Agentic authentication scope (required if USE_AGENTIC_AUTH=true) -# Example: https://api.powerplatform.com/.default +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # ============================================================================= -# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= +# These map to appsettings-style connection configuration loaded by the SDK. +# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets + +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> + +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> + +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> -# Service connection settings for Agent365 -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES= +# OAuth scope(s) for the service connection token — space-separated if multiple. +# Keep this as the Agent 365 platform scope for the blueprint token exchange. +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default -# Agent application user authorization settings +# Maps incoming Bot Framework service URLs to a connection name. +# Use * to match any service URL (recommended for most deployments). +CONNECTIONSMAP_0_SERVICEURL=* +CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION + +# Agentic user authorization handler — controls how the SDK exchanges tokens for MCP tools. +# ALT_BLUEPRINT_NAME: name of the connection used as the token exchange blueprint AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION +# Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default -# Connections map configuration -CONNECTIONSMAP_0_SERVICEURL=* -CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION - # ============================================================================= -# CLIENT CREDENTIALS AUTHENTICATION (Optional) +# CLIENT CREDENTIALS AUTHENTICATION (Optional — alternative to SERVICE_CONNECTION) # ============================================================================= -# For production deployments, use client credentials instead of bearer token +# Use these if you prefer a flat credential configuration rather than the +# CONNECTIONS__ hierarchy above. -# Azure AD Client ID +# Azure AD Client ID (Application ID) CLIENT_ID= -# Azure AD Tenant ID +# Azure AD Tenant ID (Directory ID) TENANT_ID= # Azure AD Client Secret CLIENT_SECRET= +# Azure App Service populates this automatically; leave blank for local dev WEBSITE_INSTANCE_ID= +# ============================================================================= +# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED +# ============================================================================= + +# Label shown in logs/traces to identify the environment (informational only) +ENVIRONMENT=Development + +# Hostname for a locally running custom MCP server (leave blank if not using one) +MCP_SERVER_HOST= + # ============================================================================= # SERVER CONFIGURATION # ============================================================================= -# Port to run the server on (optional, defaults to 3978) +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) PORT=3978 # ============================================================================= -# LOGGING CONFIGURATION +# LOGGING # ============================================================================= -# Logging level (optional, defaults to INFO) -# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL LOG_LEVEL=INFO # ============================================================================= -# OBSERVABILITY CONFIGURATION (Optional) +# OBSERVABILITY (Agent 365 Telemetry) # ============================================================================= -# Enable observability tracing (set to true to track agent operations) +# Master switch — enable OpenTelemetry tracing for this agent ENABLE_OBSERVABILITY=true -# Service name for observability +# Logical service name shown in traces / dashboards OBSERVABILITY_SERVICE_NAME=claude-agent -# Service namespace for observability +# Namespace grouping for this sample in telemetry backends OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples -# Enable Agent 365 Observability Exporter (optional, defaults to false) -# Set to "true" to export telemetry to Agent 365 backend for production monitoring +# Set to "true" to ship traces to the Agent 365 cloud observability backend. +# Requires a valid token and is intended for production / staging deployments. ENABLE_A365_OBSERVABILITY_EXPORTER=false +# OpenTelemetry SDK internal log level (separate from application LOG_LEVEL) OTEL_LOG_LEVEL=debug -# Python environment (influences target cluster/category) -# Options: development, production +# Python environment label — influences routing and cluster selection in Agent 365 +# Options: development | production PYTHON_ENVIRONMENT=development diff --git a/python/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index c856a2fb..2b618653 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -1,145 +1,158 @@ # ============================================================================= -# CrewAI Agent Sample - Environment Configuration +# CrewAI Agent Sample — Environment Configuration # ============================================================================= -# Copy this file to .env and fill in your values. -# Lines starting with # are comments. Remove # to enable a variable. +# Copy this file to .env and fill in ALL required values before running. +# Lines starting with # are comments. Never commit .env to source control. # ============================================================================= # ----------------------------------------------------------------------------- -# OPENAI / AZURE OPENAI CONFIGURATION +# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one) # ----------------------------------------------------------------------------- -# Choose ONE of the following configurations: -# Option A: Standard OpenAI +# --- Option A: Standard OpenAI --- # Get your API key from https://platform.openai.com/api-keys -OPENAI_API_KEY= +OPENAI_API_KEY=<> -# Option B: Azure OpenAI (recommended for enterprise) -# Get these values from Azure Portal > Your OpenAI Resource > Keys and Endpoint -AZURE_API_KEY= -AZURE_API_BASE= +# --- Option B: Azure OpenAI (recommended for enterprise) --- +# Get these from: Azure Portal > Your Azure OpenAI resource > Keys and Endpoint +AZURE_API_KEY=<> +AZURE_API_BASE=<> # e.g. https://my-resource.openai.azure.com/ AZURE_API_VERSION=2025-01-01-preview -AZURE_OPENAI_DEPLOYMENT=azure/gpt-4.1 +AZURE_OPENAI_DEPLOYMENT=azure/gpt-4.1 # e.g. azure/ -# Model Configuration -# For Azure OpenAI: Use "azure/" format (e.g., azure/gpt-4.1) -# For OpenAI: Use model name directly (e.g., gpt-4o-mini) +# Model name used by CrewAI LLM client. +# Azure OpenAI: "azure/" (e.g., azure/gpt-4.1) +# Standard OpenAI: model name directly (e.g., gpt-4o-mini) OPENAI_MODEL_NAME=azure/gpt-4.1 -# ----------------------------------------------------------------------------- -# MCP (MODEL CONTEXT PROTOCOL) AUTHENTICATION -# ----------------------------------------------------------------------------- -# These settings enable MCP tools like Mail, Calendar, and Copilot - -# Bearer Token for MCP Server Authentication -# Generate with: a365 develop get-token -o raw -# This token expires - regenerate when you see MCP connection errors -BEARER_TOKEN= - -# Agentic Authentication Mode -# Set to "false" for local development with bearer token -# Set to "true" for production with full app registration -USE_AGENTIC_AUTH=false - -AUTH_HANDLER_NAME=AGENTIC +# ============================================================================= +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) +# ============================================================================= -# Agentic authentication scope (required if USE_AGENTIC_AUTH=true) -# Example: https://api.powerplatform.com/.default -AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default +# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID +AGENTIC_APP_ID=<> -# Agent identifiers +# Agent ID — used for observability tracing and as fallback identifier. # AGENT_ID is the primary identifier used by the backend for observability. # If not set, the application automatically falls back to using AGENTIC_APP_ID. -AGENT_ID= - -# Agent Application ID (used for MCP tool discovery and as the default agent ID) -# This identifies your agent when connecting to MCP servers -AGENTIC_APP_ID=crewai-agent +AGENT_ID=<> -# ----------------------------------------------------------------------------- -# TAVILY API - WEATHER SEARCH TOOL -# ----------------------------------------------------------------------------- -# Required for the WeatherTool to search current weather conditions -# Get your free API key from https://tavily.com -TAVILY_API_KEY= +# ============================================================================= +# AUTHENTICATION +# ============================================================================= -# ----------------------------------------------------------------------------- -# OBSERVABILITY CONFIGURATION -# ----------------------------------------------------------------------------- -# These settings configure Agent 365 observability tracing +# --- Option A: Bearer Token (development / local testing) --- +# Generate with: a365 develop get-token -o raw +# This token expires — regenerate when you see 401 / MCP connection errors. +# When BEARER_TOKEN is set, agentic auth is bypassed. +BEARER_TOKEN=<> -# Service identifiers for telemetry -OBSERVABILITY_SERVICE_NAME=crewai-agent-sample -OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. +USE_AGENTIC_AUTH=false -# Enable/disable observability features -ENABLE_OBSERVABILITY=true +# Name of the auth handler configured in your app registration (default: AGENTIC) +AUTH_HANDLER_NAME=AGENTIC -# Enable Agent 365 cloud exporter (requires valid token) -# Set to "true" to send traces to Agent 365 observability backend -ENABLE_A365_OBSERVABILITY_EXPORTER=false +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default -# Python environment indicator -PYTHON_ENVIRONMENT=development +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# These map to appsettings-style connection configuration loaded by the SDK. +# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets -# ----------------------------------------------------------------------------- -# SERVER CONFIGURATION -# ----------------------------------------------------------------------------- -# The port the agent server listens on -# If busy, the server will automatically try the next available port -PORT=3978 +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> -# Logging verbosity: DEBUG, INFO, WARNING, ERROR -LOG_LEVEL=INFO +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> -# ----------------------------------------------------------------------------- -# MCP SERVER CONFIGURATION (ADVANCED) -# ----------------------------------------------------------------------------- -# These are typically not needed for standard usage +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> -# Local MCP server settings (for custom MCP server development) -MCP_SERVER_PORT= -MCP_SERVER_HOST= -MCP_DEVELOPMENT_BASE_URL= - -# ----------------------------------------------------------------------------- -# AGENT 365 APP REGISTRATION (PRODUCTION) -# ----------------------------------------------------------------------------- -# Required for production deployments with full authentication -# Get these from Azure Portal > App Registrations > Your App - -# Azure AD App Registration -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -# V2 MCP: per-audience scopes are resolved automatically by the SDK via authorization_context. -# Set this to the Agent365 platform scope for the initial blueprint token exchange. +# OAuth scope(s) for the service connection token. +# Keep this as the Agent 365 platform scope for the blueprint token exchange. CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default -# Service URL mapping +# Maps incoming Bot Framework service URLs to a connection name. +# Use * to match any service URL (recommended for most deployments). CONNECTIONSMAP__0__SERVICEURL=* CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION - -# Agent application user authorization settings +# Agentic user authorization handler — controls how the SDK exchanges tokens for MCP tools. +# ALT_BLUEPRINT_NAME: name of the connection used as the token exchange blueprint AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION +# Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default # ============================================================================= -# CLIENT CREDENTIALS AUTHENTICATION (Optional) +# CLIENT CREDENTIALS AUTHENTICATION (Optional — alternative to SERVICE_CONNECTION) # ============================================================================= -# For production deployments, use client credentials instead of bearer token -# Azure AD Client ID +# Azure AD Client ID (Application ID) CLIENT_ID= -# Azure AD Tenant ID +# Azure AD Tenant ID (Directory ID) TENANT_ID= # Azure AD Client Secret CLIENT_SECRET= +# Azure App Service populates this automatically; leave blank for local dev WEBSITE_INSTANCE_ID= +# ============================================================================= +# TAVILY API — WEATHER SEARCH TOOL (REQUIRED if using WeatherTool) +# ============================================================================= +# Required for the WeatherTool to search current weather conditions. +# Get your free API key from https://tavily.com +TAVILY_API_KEY=<> + +# ============================================================================= +# MCP SERVER CONFIGURATION (Advanced — leave blank for standard usage) +# ============================================================================= + +# Override the default MCP platform base URL +MCP_DEVELOPMENT_BASE_URL= + +# Port for a locally running custom MCP server (optional) +MCP_SERVER_PORT= + +# Hostname for a locally running custom MCP server (optional) +MCP_SERVER_HOST= + +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= + +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) +PORT=3978 + +# Log verbosity: DEBUG | INFO | WARNING | ERROR +LOG_LEVEL=INFO + +# ============================================================================= +# OBSERVABILITY (Agent 365 Telemetry) +# ============================================================================= + +# Logical service name shown in traces / dashboards +OBSERVABILITY_SERVICE_NAME=crewai-agent-sample + +# Namespace grouping for this sample in telemetry backends +OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples + +# Master switch — enable OpenTelemetry tracing for this agent +ENABLE_OBSERVABILITY=true + +# Set to "true" to ship traces to the Agent 365 cloud observability backend. +# Requires a valid token and is intended for production / staging deployments. +ENABLE_A365_OBSERVABILITY_EXPORTER=false + +# Python environment label — influences routing and cluster selection in Agent 365 +# Options: development | production +PYTHON_ENVIRONMENT=development diff --git a/python/google-adk/sample-agent/.env.template b/python/google-adk/sample-agent/.env.template index 7dcf7a82..9f39d086 100644 --- a/python/google-adk/sample-agent/.env.template +++ b/python/google-adk/sample-agent/.env.template @@ -1,61 +1,99 @@ +# ============================================================================= +# Google ADK Agent Sample — Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in ALL required values before running. +# Lines starting with # are comments. Never commit .env to source control. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# GOOGLE AI CONFIGURATION (REQUIRED) +# ----------------------------------------------------------------------------- + +# Use Google AI Studio (FALSE) or Vertex AI (TRUE) GOOGLE_GENAI_USE_VERTEXAI=FALSE -GOOGLE_API_KEY= + +# Google AI Studio API key — get it from https://aistudio.google.com/apikey +# Required when GOOGLE_GENAI_USE_VERTEXAI=FALSE +GOOGLE_API_KEY=<> # ============================================================================= -# MCP (Model Context Protocol) CONFIGURATION +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) # ============================================================================= -# MCP Platform Endpoint (optional, defaults to https://agent365.svc.cloud.microsoft) -MCP_PLATFORM_ENDPOINT= +# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID +AGENTIC_APP_ID=<> -# Bearer token for local development/testing (USE_AGENTIC_AUTH must be false) +# Agent ID — used for observability tracing and as fallback identifier. +# Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-adk-agent" +AGENT_ID=<> + +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> + +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> + +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> + +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> + +# ============================================================================= +# AUTHENTICATION +# ============================================================================= + +# --- Option A: Bearer Token (development / local testing) --- # Generate with: a365 develop get-token -o raw -BEARER_TOKEN= +# This token expires — regenerate when you see 401 / MCP connection errors. +# When BEARER_TOKEN is set, agentic auth is bypassed. +BEARER_TOKEN=<> -# Use agentic authentication for production (set to true for Teams/M365 deployment) +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=false -# Authentication handler name +# Name of the auth handler configured in your app registration (default: AGENTIC) AUTH_HANDLER_NAME=AGENTIC -# Agentic authentication scope +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # ============================================================================= -# AGENT IDENTITY +# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED # ============================================================================= -# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) -AGENTIC_APP_ID= +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# These map to appsettings-style connection configuration loaded by the SDK. +# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets -# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) -AGENT_ID= +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> -# These values are expected to be in the activity's recipient field -AGENTIC_UPN= -AGENTIC_NAME= -AGENTIC_USER_ID= -AGENTIC_TENANT_ID= +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> -# ============================================================================= -# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION -# ============================================================================= +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> -# Service connection settings (required for production agentic auth) -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= -# V2 MCP: per-audience scopes are resolved automatically by the SDK via authorization_context. -# Set this to the Agent365 platform scope for the initial blueprint token exchange. +# OAuth scope(s) for the service connection token. +# Keep this as the Agent 365 platform scope for the blueprint token exchange. CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://api.powerplatform.com/.default -# Agent application user authorization settings +# Agentic user authorization handler — controls how the SDK exchanges tokens for MCP tools. +# ALT_BLUEPRINT_NAME: name of the connection used as the token exchange blueprint AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION +# Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default -# Connections map configuration +# Maps incoming Bot Framework service URLs to a connection name. +# Use * to match any service URL (recommended for most deployments). CONNECTIONSMAP__0__SERVICEURL=* CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION @@ -63,15 +101,29 @@ CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION # SERVER CONFIGURATION # ============================================================================= +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) PORT=3978 + +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL LOG_LEVEL=INFO # ============================================================================= -# OBSERVABILITY CONFIGURATION +# OBSERVABILITY (Agent 365 Telemetry) # ============================================================================= +# Master switch — enable OpenTelemetry tracing for this agent ENABLE_OBSERVABILITY=true + +# Set to "true" to ship traces to the Agent 365 cloud observability backend. +# Requires a valid token and is intended for production / staging deployments. ENABLE_A365_OBSERVABILITY_EXPORTER=false + +# Logical service name shown in traces / dashboards OBSERVABILITY_SERVICE_NAME=google-adk-agent-sample + +# Namespace grouping for this sample in telemetry backends OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples + +# Python environment label — influences routing and cluster selection in Agent 365 +# Options: development | production PYTHON_ENVIRONMENT=development diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index e85629fa..65663da0 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -1,66 +1,100 @@ # ============================================================================= -# OPENAI / AZURE OPENAI CONFIGURATION +# OpenAI Agent Sample — Environment Configuration # ============================================================================= +# Copy this file to .env and fill in ALL required values before running. +# Lines starting with # are comments. Never commit .env to source control. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one) +# ----------------------------------------------------------------------------- -# Standard OpenAI API key +# --- Option A: Standard OpenAI --- # Get your API key from https://platform.openai.com/api-keys -OPENAI_API_KEY= +OPENAI_API_KEY=<> -# OpenAI model to use +# OpenAI model to use (e.g. gpt-4o, gpt-4o-mini) OPENAI_MODEL=gpt-4o-mini -# Azure OpenAI Configuration (use instead of OPENAI_API_KEY for Azure deployments) -AZURE_OPENAI_API_KEY= -AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +# --- Option B: Azure OpenAI (recommended for enterprise) --- +# Get these from: Azure Portal > Your Azure OpenAI resource > Keys and Endpoint +AZURE_OPENAI_API_KEY=<> +AZURE_OPENAI_ENDPOINT=<> # e.g. https://my-resource.openai.azure.com/ +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini # name of your Azure OpenAI deployment # ============================================================================= -# MCP (Model Context Protocol) CONFIGURATION +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) # ============================================================================= -# MCP Server Configuration -MCP_SERVER_PORT=8000 -MCP_SERVER_HOST=localhost -MCP_DEVELOPMENT_BASE_URL= +# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID +AGENTIC_APP_ID=<> + +# Agent ID — used for observability tracing and as fallback identifier. +# Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-openai-agent" +AGENT_ID=<> -# Bearer token for local development/testing (USE_AGENTIC_AUTH must be false) +# ============================================================================= +# AUTHENTICATION +# ============================================================================= + +# --- Option A: Bearer Token (development / local testing) --- # Generate with: a365 develop get-token -o raw -BEARER_TOKEN= +# This token expires — regenerate when you see 401 / MCP connection errors. +# When BEARER_TOKEN is set, agentic auth is bypassed. +BEARER_TOKEN=<> -# Use agentic authentication for production (set to true for Teams/M365 deployment) +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=false -# Authentication handler name +# Name of the auth handler configured in your app registration (default: AGENTIC) AUTH_HANDLER_NAME=AGENTIC -# Agentic authentication scope +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # ============================================================================= -# AGENT IDENTITY +# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED # ============================================================================= -# Agent Application ID — used for V2 MCP server discovery (/agents/v2/{id}/mcpServers) -AGENTIC_APP_ID= +# Override the default MCP platform base URL +MCP_DEVELOPMENT_BASE_URL= -# Agent ID (used for observability and as fallback for AGENTIC_APP_ID) -AGENT_ID= +# Port for a locally running custom MCP server (optional) +MCP_SERVER_PORT=8000 + +# Hostname for a locally running custom MCP server (optional) +MCP_SERVER_HOST=localhost # ============================================================================= -# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= +# These map to appsettings-style connection configuration loaded by the SDK. +# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets + +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> + +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> + +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> -# Service connection settings (required for production agentic auth) -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= -CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= +# OAuth scope(s) for the service connection token. +# Keep this as the Agent 365 platform scope for the blueprint token exchange. CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://graph.microsoft.com/.default -# Agent application user authorization settings +# Agentic user authorization handler — controls how the SDK exchanges tokens for MCP tools. +# ALT_BLUEPRINT_NAME: name of the connection used as the token exchange blueprint AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION +# Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default +# Maps incoming Bot Framework service URLs to a connection name. +# Use * to match any service URL (recommended for most deployments). CONNECTIONSMAP__0__SERVICEURL=* CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION @@ -68,15 +102,29 @@ CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION # SERVER CONFIGURATION # ============================================================================= +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) PORT=3978 + +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL LOG_LEVEL=INFO # ============================================================================= -# OBSERVABILITY CONFIGURATION +# OBSERVABILITY (Agent 365 Telemetry) # ============================================================================= +# Master switch — enable OpenTelemetry tracing for this agent ENABLE_OBSERVABILITY=true + +# Set to "true" to ship traces to the Agent 365 cloud observability backend. +# Requires a valid token and is intended for production / staging deployments. ENABLE_A365_OBSERVABILITY_EXPORTER=false + +# Logical service name shown in traces / dashboards OBSERVABILITY_SERVICE_NAME=openai-agent-sample + +# Namespace grouping for this sample in telemetry backends OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples + +# Python environment label — influences routing and cluster selection in Agent 365 +# Options: development | production PYTHON_ENVIRONMENT=development From 0e5b26bc6a70b8c4afa5a9145d76588c06d88029 Mon Sep 17 00:00:00 2001 From: biswapm Date: Fri, 10 Apr 2026 11:10:14 +0530 Subject: [PATCH 03/10] Merge branch 'main' of https://github.com/microsoft/Agent365-Samples into mcp-v1-v2-changes-python --- .../sample-agent/.env.template | 36 +- .../sample-agent/ToolingManifest.json | 41 +- .../sample-agent/tests/__init__.py | 2 - .../tests/test_tooling_manifest.py | 77 ---- python/claude/sample-agent/.env.template | 31 +- .../claude/sample-agent/ToolingManifest.json | 32 +- python/claude/sample-agent/tests/__init__.py | 2 - python/claude/sample-agent/tests/conftest.py | 43 -- .../test_mcp_tool_registration_service.py | 376 ------------------ python/crewai/sample_agent/.env.template | 31 +- .../crewai/sample_agent/ToolingManifest.json | 32 +- .../sample-agent/ToolingManifest.json | 38 +- .../google-adk/sample-agent/tests/__init__.py | 2 - .../google-adk/sample-agent/tests/conftest.py | 41 -- .../test_mcp_tool_registration_service.py | 257 ------------ python/openai/sample-agent/.env.template | 32 +- .../openai/sample-agent/ToolingManifest.json | 38 +- python/openai/sample-agent/tests/__init__.py | 2 - .../tests/test_tooling_manifest.py | 77 ---- 19 files changed, 156 insertions(+), 1034 deletions(-) delete mode 100644 python/agent-framework/sample-agent/tests/__init__.py delete mode 100644 python/agent-framework/sample-agent/tests/test_tooling_manifest.py delete mode 100644 python/claude/sample-agent/tests/__init__.py delete mode 100644 python/claude/sample-agent/tests/conftest.py delete mode 100644 python/claude/sample-agent/tests/test_mcp_tool_registration_service.py delete mode 100644 python/google-adk/sample-agent/tests/__init__.py delete mode 100644 python/google-adk/sample-agent/tests/conftest.py delete mode 100644 python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py delete mode 100644 python/openai/sample-agent/tests/__init__.py delete mode 100644 python/openai/sample-agent/tests/test_tooling_manifest.py diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index bd4e9d62..1ed030d8 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -27,7 +27,7 @@ AZURE_OPENAI_API_VERSION=2025-01-01-preview # AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) # ============================================================================= -# Agent Application ID — the GUID of your registered agent in the Agent 365 portal. +# Agent Application ID — the GUID of your registered agent in the Microsoft 365 portal. # Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID AGENTIC_APP_ID=<> @@ -35,6 +35,19 @@ AGENTIC_APP_ID=<> # Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-agent-framework-agent" AGENT_ID=<> +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> + +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> + +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> + +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> + # ============================================================================= # AUTHENTICATION # ============================================================================= @@ -49,9 +62,14 @@ BEARER_TOKEN=<> # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=true -# Name of the auth handler configured in your app registration (default: AGENTIC) +# Authentication handler: +# "AGENTIC" — production (Teams / Azure/GCP/AWS deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. AUTH_HANDLER_NAME=AGENTIC +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default + # ============================================================================= # MCP (MODEL CONTEXT PROTOCOL) — ADVANCED # ============================================================================= @@ -62,8 +80,18 @@ MCP_SERVER_HOST= # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= -# These map to appsettings-style connection configuration loaded by the SDK. -# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets +# Where to find them (after running `a365 config init`): +# CLIENTID => a365.generated.config.json → agentBlueprintId +# CLIENTSECRET => a365.generated.config.json → agentBlueprintClientSecret +# TENANTID => a365.config.json → tenantId +# +# IMPORTANT — Client Secret: +# The a365.generated.config.json stores the secret encrypted with Windows DPAPI. +# Use `a365 config display -g` to view the decrypted secret, and copy it here. +# +# IMPORTANT — Client ID and JWT Audience: +# CLIENTID is the blueprint/app-registration ID. Bot Framework tokens are issued +# with aud=CLIENTID, so this value is also used for JWT audience validation. # Azure AD Application (client) ID of your bot/agent app registration CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> diff --git a/python/agent-framework/sample-agent/ToolingManifest.json b/python/agent-framework/sample-agent/ToolingManifest.json index bfd6d353..78cef4b3 100644 --- a/python/agent-framework/sample-agent/ToolingManifest.json +++ b/python/agent-framework/sample-agent/ToolingManifest.json @@ -1,44 +1,21 @@ { "mcpServers": [ { - "mcpServerName": "mcp_Admin365_GraphTools", - "mcpServerUniqueName": "mcp_Admin365_GraphTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", - "scope": "McpServers.Admin365Graph.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_OneDriveRemoteServer", - "mcpServerUniqueName": "mcp_OneDriveRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_SharePointRemoteServer", - "mcpServerUniqueName": "mcp_SharePointRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", - "scope": "Tools.ListInvoke.All", - "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_TeamsServerV1", - "mcpServerUniqueName": "mcp_TeamsServerV1", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", - "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "scope": "Tools.ListInvoke.All", + "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", "publisher": "Microsoft" } ] -} +} \ No newline at end of file diff --git a/python/agent-framework/sample-agent/tests/__init__.py b/python/agent-framework/sample-agent/tests/__init__.py deleted file mode 100644 index 59e481eb..00000000 --- a/python/agent-framework/sample-agent/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. diff --git a/python/agent-framework/sample-agent/tests/test_tooling_manifest.py b/python/agent-framework/sample-agent/tests/test_tooling_manifest.py deleted file mode 100644 index 46f1e303..00000000 --- a/python/agent-framework/sample-agent/tests/test_tooling_manifest.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Tests for agent-framework ToolingManifest.json structure. -Validates V2 MCP fields are present and correctly formed. -""" - -import json -import os -import pytest - -MANIFEST_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "ToolingManifest.json", -) - -MCP_SERVERS_ALL_PATTERN = "McpServers." -V2_AUDIENCE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - - -@pytest.fixture(scope="module") -def manifest(): - with open(MANIFEST_PATH) as f: - return json.load(f) - - -@pytest.fixture(scope="module") -def servers(manifest): - return manifest["mcpServers"] - - -class TestManifestStructure: - def test_manifest_has_mcp_servers_key(self, manifest): - assert "mcpServers" in manifest - - def test_at_least_one_server(self, servers): - assert len(servers) > 0 - - def test_each_server_has_required_fields(self, servers): - required = {"mcpServerName", "mcpServerUniqueName", "url", "scope", "audience", "publisher"} - for s in servers: - missing = required - s.keys() - assert not missing, f"Server '{s.get('mcpServerName')}' missing fields: {missing}" - - def test_urls_are_https(self, servers): - for s in servers: - assert s["url"].startswith("https://"), f"Server '{s['mcpServerName']}' URL must be HTTPS" - - def test_urls_point_to_production_endpoint(self, servers): - for s in servers: - assert "agent365.svc.cloud.microsoft" in s["url"], ( - f"Server '{s['mcpServerName']}' should use production endpoint" - ) - - def test_no_null_scopes(self, servers): - for s in servers: - assert s["scope"] and s["scope"] != "null", ( - f"Server '{s['mcpServerName']}' has null/empty scope" - ) - - def test_mcp_servers_all_scopes_use_v2_audience(self, servers): - """Servers with McpServers.*.All scope must use the V2 audience GUID.""" - for s in servers: - if s["scope"].startswith(MCP_SERVERS_ALL_PATTERN): - assert s["audience"] == V2_AUDIENCE, ( - f"Server '{s['mcpServerName']}' with scope '{s['scope']}' " - f"must use audience '{V2_AUDIENCE}'" - ) - - def test_publisher_is_set(self, servers): - for s in servers: - assert s["publisher"], f"Server '{s['mcpServerName']}' has empty publisher" - - def test_no_duplicate_server_names(self, servers): - names = [s["mcpServerName"] for s in servers] - assert len(names) == len(set(names)), "Duplicate mcpServerName entries found" diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index ee029ed7..9f0a0697 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -28,6 +28,19 @@ AGENTIC_APP_ID=<> # Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-claude-agent" AGENT_ID=<> +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> + +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> + +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> + +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> + # Environment ID for MCP platform routing (optional, defaults to prod) # Options: dev | test | preprod | prod ENVIRONMENT_ID=prod @@ -46,7 +59,9 @@ BEARER_TOKEN=<> # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=true -# Name of the auth handler configured in your app registration (default: AGENTIC) +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. AUTH_HANDLER_NAME=AGENTIC # OAuth scope for the initial blueprint token exchange with the Agent 365 platform @@ -55,8 +70,18 @@ AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= -# These map to appsettings-style connection configuration loaded by the SDK. -# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets +# Where to find them (after running `a365 config init`): +# CLIENTID => a365.generated.config.json → agentBlueprintId +# CLIENTSECRET => a365.generated.config.json → agentBlueprintClientSecret +# TENANTID => a365.config.json → tenantId +# +# IMPORTANT — Client Secret: +# The a365.generated.config.json stores the secret encrypted with Windows DPAPI. +# Use `a365 config display -g` to view the decrypted secret, and copy it here. +# +# IMPORTANT — Client ID and JWT Audience: +# CLIENTID is the blueprint/app-registration ID. Bot Framework tokens are issued +# with aud=CLIENTID, so this value is also used for JWT audience validation. # Azure AD Application (client) ID of your bot/agent app registration CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> diff --git a/python/claude/sample-agent/ToolingManifest.json b/python/claude/sample-agent/ToolingManifest.json index 0fffc838..2ccf1029 100644 --- a/python/claude/sample-agent/ToolingManifest.json +++ b/python/claude/sample-agent/ToolingManifest.json @@ -1,35 +1,19 @@ { "mcpServers": [ { - "mcpServerName": "mcp_Admin365_GraphTools", - "mcpServerUniqueName": "mcp_Admin365_GraphTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", - "scope": "McpServers.Admin365Graph.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_OneDriveRemoteServer", - "mcpServerUniqueName": "mcp_OneDriveRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", "publisher": "Microsoft" }, { - "mcpServerName": "mcp_SharePointRemoteServer", - "mcpServerUniqueName": "mcp_SharePointRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "mcpServerName": "mcp_MailTools", + "mcpServerUniqueName": "mcp_MailTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_TeamsServerV1", - "mcpServerUniqueName": "mcp_TeamsServerV1", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", "publisher": "Microsoft" } ] diff --git a/python/claude/sample-agent/tests/__init__.py b/python/claude/sample-agent/tests/__init__.py deleted file mode 100644 index 59e481eb..00000000 --- a/python/claude/sample-agent/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. diff --git a/python/claude/sample-agent/tests/conftest.py b/python/claude/sample-agent/tests/conftest.py deleted file mode 100644 index 9565eb84..00000000 --- a/python/claude/sample-agent/tests/conftest.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Pytest configuration: mock Microsoft/Google SDK imports that may not be installed -in the test environment so unit tests can import the service modules directly. -""" - -import sys -from unittest.mock import MagicMock - - -def _mock_sdk(): - mocks = { - "microsoft_agents": MagicMock(), - "microsoft_agents.hosting": MagicMock(), - "microsoft_agents.hosting.core": MagicMock(), - "microsoft_agents_a365": MagicMock(), - "microsoft_agents_a365.tooling": MagicMock(), - "microsoft_agents_a365.tooling.utils": MagicMock(), - "microsoft_agents_a365.tooling.utils.constants": MagicMock(), - "microsoft_agents_a365.tooling.utils.utility": MagicMock(), - "microsoft_agents_a365.tooling.services": MagicMock(), - "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), - } - - # Constants.Headers stubs - headers_mock = MagicMock() - headers_mock.AUTHORIZATION = "Authorization" - headers_mock.BEARER_PREFIX = "Bearer" - mocks["microsoft_agents_a365.tooling.utils.constants"].Constants.Headers = headers_mock - - # get_mcp_platform_authentication_scope stub - mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( - lambda: ["https://api.powerplatform.com/.default"] - ) - - for name, mock in mocks.items(): - if name not in sys.modules: - sys.modules[name] = mock - - -_mock_sdk() diff --git a/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py b/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py deleted file mode 100644 index 32ea8b41..00000000 --- a/python/claude/sample-agent/tests/test_mcp_tool_registration_service.py +++ /dev/null @@ -1,376 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Unit tests for Claude MCP Tool Registration Service. - -Covers V2 MCP changes: -- authorization_context passed to list_tool_servers -- publisher and headers extracted from SDK configs and ToolingManifest.json -- Per-server header merging: {**base_headers, **server_headers} -""" - -import json -import os -import sys -import pytest -from unittest.mock import AsyncMock, MagicMock, patch - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from mcp_tool_registration_service import McpToolRegistrationService - - -# --------------------------------------------------------------------------- -# _build_full_url -# --------------------------------------------------------------------------- - -class TestBuildFullUrl: - def setup_method(self): - self.service = McpToolRegistrationService() - - def test_full_https_url_returned_unchanged(self): - url = "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test" - assert self.service._build_full_url(url) == url - - def test_full_http_url_returned_unchanged(self): - url = "http://localhost:8080/mcp" - assert self.service._build_full_url(url) == url - - def test_relative_agents_path_prepends_endpoint(self): - with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): - result = self.service._build_full_url("agents/servers/mcp_Test") - assert result == "https://my.endpoint.com/agents/servers/mcp_Test" - - def test_bare_server_name_becomes_agents_servers_path(self): - with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): - result = self.service._build_full_url("mcp_Test") - assert result == "https://my.endpoint.com/agents/servers/mcp_Test" - - def test_leading_slash_stripped(self): - with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://my.endpoint.com"}): - result = self.service._build_full_url("/agents/servers/mcp_Test") - assert result == "https://my.endpoint.com/agents/servers/mcp_Test" - - def test_empty_string_returns_empty(self): - assert self.service._build_full_url("") == "" - - -# --------------------------------------------------------------------------- -# _load_manifest_servers_fallback -# --------------------------------------------------------------------------- - -class TestLoadManifestServersFallback: - def setup_method(self): - self.service = McpToolRegistrationService() - - def test_loads_all_v2_fields(self, tmp_path): - manifest = { - "mcpServers": [{ - "mcpServerName": "mcp_Test", - "mcpServerUniqueName": "mcp_Test", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test", - "scope": "McpServers.Test.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft", - "headers": {"X-Custom": "value"}, - }] - } - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - - assert len(servers) == 1 - s = servers[0] - assert s["name"] == "mcp_Test" - assert s["scope"] == "McpServers.Test.All" - assert s["audience"] == "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - assert s["publisher"] == "Microsoft" - assert s["headers"] == {"X-Custom": "value"} - - def test_skips_servers_without_url(self, tmp_path): - manifest = {"mcpServers": [{"mcpServerName": "mcp_NoUrl"}]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - - assert servers == [] - - def test_defaults_publisher_and_headers_when_absent(self, tmp_path): - manifest = {"mcpServers": [{ - "mcpServerName": "mcp_Min", - "url": "https://example.com/mcp_Min", - }]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - - assert servers[0]["publisher"] == "" - assert servers[0]["headers"] == {} - - def test_returns_empty_when_manifest_missing(self, tmp_path): - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - assert servers == [] - - def test_multiple_servers_loaded(self, tmp_path): - manifest = {"mcpServers": [ - {"mcpServerName": "mcp_A", "url": "https://example.com/A"}, - {"mcpServerName": "mcp_B", "url": "https://example.com/B"}, - ]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - - assert len(servers) == 2 - - -# --------------------------------------------------------------------------- -# _connect_to_server -# --------------------------------------------------------------------------- - -class TestConnectToServer: - def setup_method(self): - self.service = McpToolRegistrationService() - - @pytest.mark.asyncio - async def test_v2_server_headers_override_base_auth_token(self): - """Per-audience token in server_headers must override the base auth_token.""" - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token="base-token", - server_headers={"Authorization": "Bearer per-audience-token"}, - ) - - assert captured["headers"]["Authorization"] == "Bearer per-audience-token" - - @pytest.mark.asyncio - async def test_base_token_used_when_no_server_headers(self): - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token="base-token", - server_headers={}, - ) - - assert "Bearer base-token" in captured["headers"].get("Authorization", "") - - @pytest.mark.asyncio - async def test_additional_server_headers_merged(self): - """Extra custom headers from server_headers should appear in final headers.""" - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token="base-token", - server_headers={"X-Tenant": "tenant-123"}, - ) - - assert captured["headers"]["X-Tenant"] == "tenant-123" - assert "Bearer base-token" in captured["headers"].get("Authorization", "") - - @pytest.mark.asyncio - async def test_returns_none_for_remote_with_no_auth(self): - result = await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token=None, - server_headers={}, - ) - assert result is None - - @pytest.mark.asyncio - async def test_local_server_connects_without_token(self): - async def mock_list(url, headers, name): - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - result = await self.service._connect_to_server( - name="local", - url="http://localhost:9999/mcp", - auth_token=None, - server_headers={}, - ) - - assert result is not None - assert result.connected is True - - @pytest.mark.asyncio - async def test_returns_none_when_server_headers_have_auth_but_auth_token_none(self): - """V2: server_headers with Authorization should allow connection even if auth_token is None.""" - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - result = await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token=None, - server_headers={"Authorization": "Bearer per-audience-token"}, - ) - - # Should NOT return None since server_headers provides auth - assert result is not None - assert captured["headers"]["Authorization"] == "Bearer per-audience-token" - - -# --------------------------------------------------------------------------- -# discover_and_connect_servers — authorization_context & V2 field extraction -# --------------------------------------------------------------------------- - -class TestDiscoverAndConnectServers: - def setup_method(self): - self.service = McpToolRegistrationService() - - @pytest.mark.asyncio - async def test_passes_authorization_context_to_sdk(self): - mock_auth = MagicMock() - mock_context = MagicMock() - captured = {} - - async def mock_list(**kwargs): - captured.update(kwargs) - return [] - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert "authorization_context" in captured - ctx = captured["authorization_context"] - assert ctx["auth"] is mock_auth - assert ctx["auth_handler_name"] == "AGENTIC" - assert ctx["context"] is mock_context - - @pytest.mark.asyncio - async def test_extracts_v2_fields_from_sdk_configs(self): - mock_auth = MagicMock() - mock_context = MagicMock() - - cfg = MagicMock() - cfg.url = "https://example.com/servers/mcp_Test" - cfg.mcp_server_name = "mcp_Test" - cfg.mcp_server_unique_name = "mcp_Test" - cfg.audience = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - cfg.scope = "McpServers.Test.All" - cfg.publisher = "Microsoft" - cfg.headers = {"Authorization": "Bearer per-audience-token"} - - async def mock_list(**kwargs): - return [cfg] - - connect_calls = [] - - async def mock_connect(name, url, auth_token, server_headers=None): - connect_calls.append({"name": name, "server_headers": server_headers}) - conn = MagicMock() - conn.connected = True - conn.tools = [] - conn.url = url - return conn - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert len(connect_calls) == 1 - assert connect_calls[0]["server_headers"] == {"Authorization": "Bearer per-audience-token"} - - @pytest.mark.asyncio - async def test_falls_back_to_manifest_when_sdk_fails(self, tmp_path): - mock_auth = MagicMock() - mock_context = MagicMock() - - manifest = {"mcpServers": [{ - "mcpServerName": "mcp_Fallback", - "url": "http://localhost:9999/mcp", - "scope": "McpServers.Test.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft", - }]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - async def sdk_fails(**kwargs): - raise RuntimeError("SDK unavailable") - - async def mock_connect(name, url, auth_token, server_headers=None): - conn = MagicMock() - conn.connected = True - conn.tools = [] - conn.url = url - conn.name = name - conn.headers = server_headers or {} - return conn - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=sdk_fails): - with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): - with patch("os.getcwd", return_value=str(tmp_path)): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert len(self.service._connected_servers) == 1 - assert self.service._connected_servers[0].name == "mcp_Fallback" - - @pytest.mark.asyncio - async def test_agentic_app_id_passed_to_sdk(self): - mock_auth = MagicMock() - mock_context = MagicMock() - captured = {} - - async def mock_list(**kwargs): - captured.update(kwargs) - return [] - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): - await self.service.discover_and_connect_servers( - agentic_app_id="my-unique-app-id", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert captured.get("agentic_app_id") == "my-unique-app-id" diff --git a/python/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index 2b618653..407fb80a 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -38,6 +38,19 @@ AGENTIC_APP_ID=<> # If not set, the application automatically falls back to using AGENTIC_APP_ID. AGENT_ID=<> +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> + +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> + +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> + +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> + # ============================================================================= # AUTHENTICATION # ============================================================================= @@ -52,7 +65,9 @@ BEARER_TOKEN=<> # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=false -# Name of the auth handler configured in your app registration (default: AGENTIC) +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. AUTH_HANDLER_NAME=AGENTIC # OAuth scope for the initial blueprint token exchange with the Agent 365 platform @@ -61,8 +76,18 @@ AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= -# These map to appsettings-style connection configuration loaded by the SDK. -# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets +# Where to find them (after running `a365 config init`): +# CLIENTID => a365.generated.config.json → agentBlueprintId +# CLIENTSECRET => a365.generated.config.json → agentBlueprintClientSecret +# TENANTID => a365.config.json → tenantId +# +# IMPORTANT — Client Secret: +# The a365.generated.config.json stores the secret encrypted with Windows DPAPI. +# Use `a365 config display -g` to view the decrypted secret, and copy it here. +# +# IMPORTANT — Client ID and JWT Audience: +# CLIENTID is the blueprint/app-registration ID. Bot Framework tokens are issued +# with aud=CLIENTID, so this value is also used for JWT audience validation. # Azure AD Application (client) ID of your bot/agent app registration CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> diff --git a/python/crewai/sample_agent/ToolingManifest.json b/python/crewai/sample_agent/ToolingManifest.json index 0fffc838..2ccf1029 100644 --- a/python/crewai/sample_agent/ToolingManifest.json +++ b/python/crewai/sample_agent/ToolingManifest.json @@ -1,35 +1,19 @@ { "mcpServers": [ { - "mcpServerName": "mcp_Admin365_GraphTools", - "mcpServerUniqueName": "mcp_Admin365_GraphTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", - "scope": "McpServers.Admin365Graph.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_OneDriveRemoteServer", - "mcpServerUniqueName": "mcp_OneDriveRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", + "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", "publisher": "Microsoft" }, { - "mcpServerName": "mcp_SharePointRemoteServer", - "mcpServerUniqueName": "mcp_SharePointRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", + "mcpServerName": "mcp_MailTools", + "mcpServerUniqueName": "mcp_MailTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_TeamsServerV1", - "mcpServerUniqueName": "mcp_TeamsServerV1", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", "publisher": "Microsoft" } ] diff --git a/python/google-adk/sample-agent/ToolingManifest.json b/python/google-adk/sample-agent/ToolingManifest.json index bfd6d353..2ccf1029 100644 --- a/python/google-adk/sample-agent/ToolingManifest.json +++ b/python/google-adk/sample-agent/ToolingManifest.json @@ -1,43 +1,19 @@ { "mcpServers": [ { - "mcpServerName": "mcp_Admin365_GraphTools", - "mcpServerUniqueName": "mcp_Admin365_GraphTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", - "scope": "McpServers.Admin365Graph.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_OneDriveRemoteServer", - "mcpServerUniqueName": "mcp_OneDriveRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_SharePointRemoteServer", - "mcpServerUniqueName": "mcp_SharePointRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", - "scope": "Tools.ListInvoke.All", - "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_TeamsServerV1", - "mcpServerUniqueName": "mcp_TeamsServerV1", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", - "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "scope": "Tools.ListInvoke.All", + "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", "publisher": "Microsoft" } ] diff --git a/python/google-adk/sample-agent/tests/__init__.py b/python/google-adk/sample-agent/tests/__init__.py deleted file mode 100644 index 59e481eb..00000000 --- a/python/google-adk/sample-agent/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. diff --git a/python/google-adk/sample-agent/tests/conftest.py b/python/google-adk/sample-agent/tests/conftest.py deleted file mode 100644 index 0eb63402..00000000 --- a/python/google-adk/sample-agent/tests/conftest.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Pytest configuration: mock Google ADK and Microsoft SDK imports that may not be installed -in the test environment so unit tests can import the service modules directly. -""" - -import sys -from unittest.mock import MagicMock - - -def _mock_sdk(): - mocks = { - "google": MagicMock(), - "google.adk": MagicMock(), - "google.adk.agents": MagicMock(), - "google.adk.tools": MagicMock(), - "google.adk.tools.mcp_tool": MagicMock(), - "google.adk.tools.mcp_tool.mcp_toolset": MagicMock(), - "microsoft_agents": MagicMock(), - "microsoft_agents.hosting": MagicMock(), - "microsoft_agents.hosting.core": MagicMock(), - "microsoft_agents_a365": MagicMock(), - "microsoft_agents_a365.tooling": MagicMock(), - "microsoft_agents_a365.tooling.utils": MagicMock(), - "microsoft_agents_a365.tooling.utils.utility": MagicMock(), - "microsoft_agents_a365.tooling.services": MagicMock(), - "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), - } - - mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( - lambda: ["https://api.powerplatform.com/.default"] - ) - - for name, mock in mocks.items(): - if name not in sys.modules: - sys.modules[name] = mock - - -_mock_sdk() diff --git a/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py b/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py deleted file mode 100644 index 2ff660c6..00000000 --- a/python/google-adk/sample-agent/tests/test_mcp_tool_registration_service.py +++ /dev/null @@ -1,257 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Unit tests for Google ADK MCP Tool Registration Service. - -Covers V2 MCP changes: -- authorization_context passed to list_tool_servers -- Per-server header merging: {**base_headers, **server_config.headers} -- Server URL resolved from server_config.url (V2) over mcp_server_unique_name (V1) -""" - -import os -import sys -import pytest -from unittest.mock import AsyncMock, MagicMock, patch - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# Patch McpToolset and StreamableHTTPConnectionParams before importing the module -import sys -from unittest.mock import MagicMock - -McpToolsetMock = MagicMock -StreamableHTTPConnectionParamsMock = MagicMock - -import mcp_tool_registration_service as svc_module -svc_module.McpToolset = McpToolsetMock -svc_module.StreamableHTTPConnectionParams = StreamableHTTPConnectionParamsMock - -from mcp_tool_registration_service import McpToolRegistrationService - - -class TestAddToolServersToAgent: - def setup_method(self): - self.service = McpToolRegistrationService() - - def _make_agent(self, tools=None): - agent = MagicMock() - agent.name = "test-agent" - agent.model = "gemini-pro" - agent.description = "Test agent" - agent.tools = tools or [] - return agent - - def _make_server_config(self, name="mcp_Test", url=None, headers=None): - cfg = MagicMock() - cfg.mcp_server_unique_name = name - cfg.url = url - cfg.headers = headers or {} - return cfg - - @pytest.mark.asyncio - async def test_passes_authorization_context_to_sdk(self): - mock_auth = MagicMock() - mock_context = MagicMock() - token_result = MagicMock() - token_result.token = "exchange-token" - mock_auth.exchange_token = AsyncMock(return_value=token_result) - captured = {} - - async def mock_list(**kwargs): - captured.update(kwargs) - return [] - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - await self.service.add_tool_servers_to_agent( - agent=self._make_agent(), - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - ) - - assert "authorization_context" in captured - ctx = captured["authorization_context"] - assert ctx["auth"] is mock_auth - assert ctx["auth_handler_name"] == "AGENTIC" - assert ctx["context"] is mock_context - - @pytest.mark.asyncio - async def test_per_server_headers_override_base_headers(self): - mock_auth = MagicMock() - mock_context = MagicMock() - token_result = MagicMock() - token_result.token = "base-token" - mock_auth.exchange_token = AsyncMock(return_value=token_result) - - cfg = self._make_server_config( - url="https://example.com/servers/mcp_Test", - headers={"Authorization": "Bearer per-audience-token", "X-Custom": "val"}, - ) - - async def mock_list(**kwargs): - return [cfg] - - captured_params = [] - - def mock_toolset(connection_params): - captured_params.append(connection_params) - return MagicMock() - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): - # Capture StreamableHTTPConnectionParams calls - param_calls = [] - - def mock_params(url, headers): - param_calls.append({"url": url, "headers": headers}) - return MagicMock() - - with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): - await self.service.add_tool_servers_to_agent( - agent=self._make_agent(), - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - ) - - assert len(param_calls) == 1 - headers = param_calls[0]["headers"] - # Per-audience token overrides base token - assert headers["Authorization"] == "Bearer per-audience-token" - assert headers["X-Custom"] == "val" - - @pytest.mark.asyncio - async def test_base_token_used_when_no_server_headers(self): - mock_auth = MagicMock() - mock_context = MagicMock() - token_result = MagicMock() - token_result.token = "base-token" - mock_auth.exchange_token = AsyncMock(return_value=token_result) - - cfg = self._make_server_config(url="https://example.com/servers/mcp_Test", headers={}) - - async def mock_list(**kwargs): - return [cfg] - - param_calls = [] - - def mock_params(url, headers): - param_calls.append({"url": url, "headers": headers}) - return MagicMock() - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): - with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): - await self.service.add_tool_servers_to_agent( - agent=self._make_agent(), - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - ) - - assert "Bearer base-token" in param_calls[0]["headers"]["Authorization"] - - @pytest.mark.asyncio - async def test_uses_server_config_url_over_unique_name(self): - """V2: server_config.url takes priority over mcp_server_unique_name.""" - mock_auth = MagicMock() - mock_context = MagicMock() - token_result = MagicMock() - token_result.token = "tok" - mock_auth.exchange_token = AsyncMock(return_value=token_result) - - cfg = self._make_server_config( - name="mcp_Test", - url="https://full-v2-url.example.com/servers/mcp_Test", - ) - - async def mock_list(**kwargs): - return [cfg] - - param_calls = [] - - def mock_params(url, headers): - param_calls.append({"url": url}) - return MagicMock() - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(svc_module, "McpToolset", side_effect=lambda connection_params: MagicMock()): - with patch.object(svc_module, "StreamableHTTPConnectionParams", side_effect=mock_params): - await self.service.add_tool_servers_to_agent( - agent=self._make_agent(), - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - ) - - assert param_calls[0]["url"] == "https://full-v2-url.example.com/servers/mcp_Test" - - @pytest.mark.asyncio - async def test_skips_token_exchange_when_auth_token_provided(self): - mock_auth = MagicMock() - mock_context = MagicMock() - mock_auth.exchange_token = AsyncMock() - - async def mock_list(**kwargs): - return [] - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - await self.service.add_tool_servers_to_agent( - agent=self._make_agent(), - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="pre-provided", - ) - - mock_auth.exchange_token.assert_not_called() - - @pytest.mark.asyncio - async def test_returns_agent_with_mcp_tools_appended(self): - mock_auth = MagicMock() - mock_context = MagicMock() - token_result = MagicMock() - token_result.token = "tok" - mock_auth.exchange_token = AsyncMock(return_value=token_result) - - cfgs = [ - self._make_server_config(name="mcp_A", url="https://example.com/A"), - self._make_server_config(name="mcp_B", url="https://example.com/B"), - ] - - async def mock_list(**kwargs): - return cfgs - - fake_tool = MagicMock() - existing_tool = MagicMock() - agent = self._make_agent(tools=[existing_tool]) - - agent_ctor_calls = [] - - def mock_agent_ctor(**kwargs): - agent_ctor_calls.append(kwargs) - return MagicMock() - - with patch.object(self.service.config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(svc_module, "McpToolset", return_value=fake_tool): - with patch.object(svc_module, "StreamableHTTPConnectionParams", return_value=MagicMock()): - with patch.object(svc_module, "Agent", side_effect=mock_agent_ctor): - await self.service.add_tool_servers_to_agent( - agent=agent, - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - ) - - # Agent constructor called with original tool + 2 MCP toolsets - assert len(agent_ctor_calls) == 1 - tools_passed = agent_ctor_calls[0]["tools"] - assert len(tools_passed) == 3 # 1 existing + 2 MCP diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index 65663da0..39f33a5e 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -34,6 +34,19 @@ AGENTIC_APP_ID=<> # Typically the same as AGENTIC_APP_ID, or a human-readable slug like "my-openai-agent" AGENT_ID=<> +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> + +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> + +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> + +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> + # ============================================================================= # AUTHENTICATION # ============================================================================= @@ -48,7 +61,9 @@ BEARER_TOKEN=<> # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. USE_AGENTIC_AUTH=false -# Name of the auth handler configured in your app registration (default: AGENTIC) +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. AUTH_HANDLER_NAME=AGENTIC # OAuth scope for the initial blueprint token exchange with the Agent 365 platform @@ -70,8 +85,18 @@ MCP_SERVER_HOST=localhost # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= -# These map to appsettings-style connection configuration loaded by the SDK. -# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets +# Where to find them (after running `a365 config init`): +# CLIENTID => a365.generated.config.json → agentBlueprintId +# CLIENTSECRET => a365.generated.config.json → agentBlueprintClientSecret +# TENANTID => a365.config.json → tenantId +# +# IMPORTANT — Client Secret: +# The a365.generated.config.json stores the secret encrypted with Windows DPAPI. +# Use `a365 config display -g` to view the decrypted secret, and copy it here. +# +# IMPORTANT — Client ID and JWT Audience: +# CLIENTID is the blueprint/app-registration ID. Bot Framework tokens are issued +# with aud=CLIENTID, so this value is also used for JWT audience validation. # Azure AD Application (client) ID of your bot/agent app registration CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> @@ -92,6 +117,7 @@ AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUs AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALT_BLUEPRINT_NAME=SERVICE_CONNECTION # Scopes requested when calling Graph / MCP APIs on behalf of the user AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default +AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default # Maps incoming Bot Framework service URLs to a connection name. # Use * to match any service URL (recommended for most deployments). diff --git a/python/openai/sample-agent/ToolingManifest.json b/python/openai/sample-agent/ToolingManifest.json index bfd6d353..2ccf1029 100644 --- a/python/openai/sample-agent/ToolingManifest.json +++ b/python/openai/sample-agent/ToolingManifest.json @@ -1,43 +1,19 @@ { "mcpServers": [ { - "mcpServerName": "mcp_Admin365_GraphTools", - "mcpServerUniqueName": "mcp_Admin365_GraphTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools", - "scope": "McpServers.Admin365Graph.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_OneDriveRemoteServer", - "mcpServerUniqueName": "mcp_OneDriveRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer", + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_SharePointRemoteServer", - "mcpServerUniqueName": "mcp_SharePointRemoteServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer", - "scope": "Tools.ListInvoke.All", - "audience": "b154d24d-a357-4961-ba54-65e171c9cb05", - "publisher": "Microsoft" - }, - { - "mcpServerName": "mcp_TeamsServerV1", - "mcpServerUniqueName": "mcp_TeamsServerV1", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", - "scope": "McpServers.Mail.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", + "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "scope": "Tools.ListInvoke.All", + "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", "publisher": "Microsoft" } ] diff --git a/python/openai/sample-agent/tests/__init__.py b/python/openai/sample-agent/tests/__init__.py deleted file mode 100644 index 59e481eb..00000000 --- a/python/openai/sample-agent/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. diff --git a/python/openai/sample-agent/tests/test_tooling_manifest.py b/python/openai/sample-agent/tests/test_tooling_manifest.py deleted file mode 100644 index f8995b34..00000000 --- a/python/openai/sample-agent/tests/test_tooling_manifest.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Tests for openai ToolingManifest.json structure. -Validates V2 MCP fields are present and correctly formed. -""" - -import json -import os -import pytest - -MANIFEST_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "ToolingManifest.json", -) - -MCP_SERVERS_ALL_PATTERN = "McpServers." -V2_AUDIENCE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - - -@pytest.fixture(scope="module") -def manifest(): - with open(MANIFEST_PATH) as f: - return json.load(f) - - -@pytest.fixture(scope="module") -def servers(manifest): - return manifest["mcpServers"] - - -class TestManifestStructure: - def test_manifest_has_mcp_servers_key(self, manifest): - assert "mcpServers" in manifest - - def test_at_least_one_server(self, servers): - assert len(servers) > 0 - - def test_each_server_has_required_fields(self, servers): - required = {"mcpServerName", "mcpServerUniqueName", "url", "scope", "audience", "publisher"} - for s in servers: - missing = required - s.keys() - assert not missing, f"Server '{s.get('mcpServerName')}' missing fields: {missing}" - - def test_urls_are_https(self, servers): - for s in servers: - assert s["url"].startswith("https://"), f"Server '{s['mcpServerName']}' URL must be HTTPS" - - def test_urls_point_to_production_endpoint(self, servers): - for s in servers: - assert "agent365.svc.cloud.microsoft" in s["url"], ( - f"Server '{s['mcpServerName']}' should use production endpoint" - ) - - def test_no_null_scopes(self, servers): - for s in servers: - assert s["scope"] and s["scope"] != "null", ( - f"Server '{s['mcpServerName']}' has null/empty scope" - ) - - def test_mcp_servers_all_scopes_use_v2_audience(self, servers): - """Servers with McpServers.*.All scope must use the V2 audience GUID.""" - for s in servers: - if s["scope"].startswith(MCP_SERVERS_ALL_PATTERN): - assert s["audience"] == V2_AUDIENCE, ( - f"Server '{s['mcpServerName']}' with scope '{s['scope']}' " - f"must use audience '{V2_AUDIENCE}'" - ) - - def test_publisher_is_set(self, servers): - for s in servers: - assert s["publisher"], f"Server '{s['mcpServerName']}' has empty publisher" - - def test_no_duplicate_server_names(self, servers): - names = [s["mcpServerName"] for s in servers] - assert len(names) == len(set(names)), "Duplicate mcpServerName entries found" From 6f3d5cf843a290b194d294a2222a26be0a51ef4b Mon Sep 17 00:00:00 2001 From: biswapm Date: Fri, 10 Apr 2026 21:40:03 +0530 Subject: [PATCH 04/10] Fixed Sample to test MCP V2 E2E Google ADK --- .../sample-agent/ToolingManifest.json | 2 +- python/google-adk/sample-agent/agent.py | 4 +-- python/google-adk/sample-agent/main.py | 27 +++++++++++++++++- .../mcp_tool_registration_service.py | 28 ++++++++++--------- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/python/google-adk/sample-agent/ToolingManifest.json b/python/google-adk/sample-agent/ToolingManifest.json index 2ccf1029..c051e793 100644 --- a/python/google-adk/sample-agent/ToolingManifest.json +++ b/python/google-adk/sample-agent/ToolingManifest.json @@ -17,4 +17,4 @@ "publisher": "Microsoft" } ] -} +} \ No newline at end of file diff --git a/python/google-adk/sample-agent/agent.py b/python/google-adk/sample-agent/agent.py index 23c6d61e..66ed1906 100644 --- a/python/google-adk/sample-agent/agent.py +++ b/python/google-adk/sample-agent/agent.py @@ -126,9 +126,7 @@ async def invoke_agent( responses = [] try: - result = await runner.run_debug( - user_messages=[message] - ) + result = await runner.run_debug(message) except Exception as e: logger.error("run_debug failed: %s", e) await self._cleanup_agent(agent) diff --git a/python/google-adk/sample-agent/main.py b/python/google-adk/sample-agent/main.py index d3378700..88c62db5 100644 --- a/python/google-adk/sample-agent/main.py +++ b/python/google-adk/sample-agent/main.py @@ -3,6 +3,14 @@ # Internal imports import os +import sys + +# Force UTF-8 on Windows so emoji/unicode in SDK log messages don't crash the charmap codec +if sys.platform == "win32": + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + from hosting import MyAgent from agent import GoogleADKAgent @@ -113,7 +121,7 @@ async def health_check(req: Request) -> Response: try: host = "0.0.0.0" if isProduction else "localhost" - + # PORT environment variable is optional - defaults to 3978 for local dev # Azure App Service automatically sets PORT=8000 port_str = os.getenv("PORT") @@ -127,6 +135,23 @@ async def health_check(req: Request) -> Response: else: port = 3978 logger.info("PORT not set, using default: %d", port) + + # Free the port if already in use — prevents [Errno 10048] on Windows restart + import subprocess as _sp, sys as _sys + if _sys.platform == "win32": + try: + _out = _sp.check_output( + f'netstat -ano', shell=True, text=True, stderr=_sp.DEVNULL + ) + for _line in _out.splitlines(): + if f":{port} " in _line and "LISTENING" in _line: + _pid = _line.split()[-1] + if _pid.isdigit(): + _sp.run(f"taskkill /PID {_pid} /F", + shell=True, capture_output=True) + logger.info("Released port %d (killed PID %s)", port, _pid) + except Exception: + pass logger.info("Listening on %s:%d/api/messages", host, port) run_app(app, host=host, port=port, handle_signals=True) diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index 7fea8168..7569708e 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import os from typing import Optional import logging @@ -55,14 +56,7 @@ async def add_tool_servers_to_agent( New Agent instance with all MCP servers """ - # Build authorization context for V2 per-audience token acquisition - authorization_context = { - "auth": auth, - "auth_handler_name": auth_handler_name, - "context": context, - } - - # V1 fallback: exchange a shared token if no bearer token provided + # Acquire auth token if not provided if not auth_token: scopes = get_mcp_platform_authentication_scope() auth_token_obj = await auth.exchange_token(context, scopes, auth_handler_name) @@ -71,7 +65,7 @@ async def add_tool_servers_to_agent( 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, - authorization_context=authorization_context, + auth_token=auth_token or "", ) self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations") @@ -92,16 +86,24 @@ async def add_tool_servers_to_agent( ) continue - # V2: merge per-server headers (server_config.headers override base_headers) - server_level_headers = getattr(server_config, "headers", None) or {} - mcp_server_headers = {**base_headers, **server_level_headers} + # V2: look up per-server token from env (BEARER_TOKEN_MCP_{SERVERNAME_UPPER}) + # e.g. mcp_CalendarTools → BEARER_TOKEN_MCP_CALENDARTOOLS + env_key = f"BEARER_TOKEN_{server_config.mcp_server_unique_name.upper()}" + per_server_token = os.getenv(env_key) + if per_server_token: + mcp_server_headers = {"Authorization": f"Bearer {per_server_token}"} + else: + # Fall back: merge base headers with any server_config.headers (V1 path) + server_level_headers = getattr(server_config, "headers", None) or {} + mcp_server_headers = {**base_headers, **server_level_headers} server_url = getattr(server_config, "url", None) or server_config.mcp_server_unique_name server_info = McpToolset( connection_params=StreamableHTTPConnectionParams( url=server_url, - headers=mcp_server_headers + headers=mcp_server_headers, + timeout=30.0, ) ) From 1baeaba24d6160500786e58747e8d478e365945a Mon Sep 17 00:00:00 2001 From: biswapm Date: Sun, 12 Apr 2026 13:18:54 +0530 Subject: [PATCH 05/10] Sample changes to support MCP tools call in local --- .../mcp_tool_registration_service.py | 35 ++++++++--- .../mcp_tool_registration_service.py | 35 ++++++++--- .../mcp_tool_registration_service.py | 61 ++++++++++++------- python/google-adk/sample-agent/pyproject.toml | 1 + 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/python/claude/sample-agent/mcp_tool_registration_service.py b/python/claude/sample-agent/mcp_tool_registration_service.py index f47173dc..6afdc34e 100644 --- a/python/claude/sample-agent/mcp_tool_registration_service.py +++ b/python/claude/sample-agent/mcp_tool_registration_service.py @@ -23,6 +23,7 @@ import asyncio from microsoft_agents.hosting.core import Authorization, TurnContext +from microsoft_agents_a365.tooling.models import ToolOptions from microsoft_agents_a365.tooling.utils.constants import Constants from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( McpToolServerConfigurationService, @@ -255,16 +256,24 @@ async def discover_and_connect_servers( try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") - # Build authorization context for V2 per-audience token acquisition - authorization_context = { - "auth": auth, - "auth_handler_name": auth_handler_name, - "context": context, - } + # Pass auth context for V2 per-audience token acquisition (production path). + # In dev/Playground mode (empty auth_handler_name), the SDK reads per-server + # tokens from BEARER_TOKEN_MCP_ / BEARER_TOKEN env vars automatically + # via its internal _attach_dev_tokens method. + options = ToolOptions(orchestrator_name=self._orchestrator_name) + list_kwargs = {} + if auth_handler_name: + list_kwargs = { + "authorization": auth, + "auth_handler_name": auth_handler_name, + "turn_context": context, + } sdk_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, - authorization_context=authorization_context, + auth_token=auth_token or "", + options=options, + **list_kwargs, ) # Convert SDK config objects to our format @@ -377,11 +386,19 @@ async def _connect_to_server( } self._logger.info(f"🏠 Connecting to local MCP server: {url}") else: - if not auth_token and not server_headers: + # server_headers contains the per-audience Authorization token set by the SDK: + # - Dev mode: set by SDK's _attach_dev_tokens (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) + # - Prod mode: set by SDK's _attach_per_audience_tokens (per-audience OAuth exchange) + # auth_token is kept as a final fallback for backward compatibility. + sdk_auth = (server_headers or {}).get(Constants.Headers.AUTHORIZATION) + effective_auth = sdk_auth or ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" if auth_token else None + ) + if not effective_auth: self._logger.warning(f"âš ī¸ Skipping remote server {name} - no auth token") return None base_headers = { - Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", + Constants.Headers.AUTHORIZATION: effective_auth, "User-Agent": f"Claude-Agent-SDK/1.0 ({self._orchestrator_name})", "Content-Type": "application/json", } diff --git a/python/crewai/sample_agent/mcp_tool_registration_service.py b/python/crewai/sample_agent/mcp_tool_registration_service.py index 657f3739..8cc7cd0e 100644 --- a/python/crewai/sample_agent/mcp_tool_registration_service.py +++ b/python/crewai/sample_agent/mcp_tool_registration_service.py @@ -26,6 +26,7 @@ import json from microsoft_agents.hosting.core import Authorization, TurnContext +from microsoft_agents_a365.tooling.models import ToolOptions from microsoft_agents_a365.tooling.utils.constants import Constants from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( McpToolServerConfigurationService, @@ -240,16 +241,24 @@ async def discover_and_connect_servers( try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") - # Build authorization context for V2 per-audience token acquisition - authorization_context = { - "auth": auth, - "auth_handler_name": auth_handler_name, - "context": context, - } + # Pass auth context for V2 per-audience token acquisition (production path). + # In dev/Playground mode (empty auth_handler_name), the SDK reads per-server + # tokens from BEARER_TOKEN_MCP_ / BEARER_TOKEN env vars automatically + # via its internal _attach_dev_tokens method. + options = ToolOptions(orchestrator_name=self._orchestrator_name) + list_kwargs = {} + if auth_handler_name: + list_kwargs = { + "authorization": auth, + "auth_handler_name": auth_handler_name, + "turn_context": context, + } sdk_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, - authorization_context=authorization_context, + auth_token=auth_token or "", + options=options, + **list_kwargs, ) # Convert SDK config objects to our format @@ -371,11 +380,19 @@ async def _connect_to_server( } self._logger.info(f"🏠 Connecting to local MCP server: {url}") else: - if not auth_token and not server_headers: + # server_headers contains the per-audience Authorization token set by the SDK: + # - Dev mode: set by SDK's _attach_dev_tokens (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) + # - Prod mode: set by SDK's _attach_per_audience_tokens (per-audience OAuth exchange) + # auth_token is kept as a final fallback for backward compatibility. + sdk_auth = (server_headers or {}).get(Constants.Headers.AUTHORIZATION) + effective_auth = sdk_auth or ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" if auth_token else None + ) + if not effective_auth: self._logger.warning(f"âš ī¸ Skipping remote server {name} - no auth token") return None base_headers = { - Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", + Constants.Headers.AUTHORIZATION: effective_auth, "User-Agent": f"CrewAI-Agent-SDK/1.0 ({self._orchestrator_name})", "Content-Type": "application/json", } diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index 7569708e..cc851c02 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import os from typing import Optional import logging @@ -10,17 +9,22 @@ from microsoft_agents.hosting.core import Authorization, TurnContext +from microsoft_agents_a365.runtime.utility import Utility +from microsoft_agents_a365.tooling.models import ToolOptions from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( McpToolServerConfigurationService, ) - +from microsoft_agents_a365.tooling.utils.constants import Constants from microsoft_agents_a365.tooling.utils.utility import ( get_mcp_platform_authentication_scope, ) + class McpToolRegistrationService: """Service for managing MCP tools and servers for an agent""" + _orchestrator_name: str = "GoogleADK" + def __init__(self, logger: Optional[logging.Logger] = None): """ Initialize the MCP Tool Registration Service for Google ADK. @@ -49,13 +53,14 @@ async def add_tool_servers_to_agent( agent: The existing agent to add servers to. agentic_app_id: Agentic App ID for the agent. auth: Authorization object used to exchange tokens for MCP server access. + auth_handler_name: Name of the authorization handler. context: TurnContext object representing the current turn/session context. - auth_token: Authentication token to access the MCP servers. If not provided, will be obtained using `auth` and `context`. + auth_token: Authentication token to access the MCP servers. If not provided, + will be obtained using `auth` and `context`. Returns: New Agent instance with all MCP servers """ - # Acquire auth token if not provided if not auth_token: scopes = get_mcp_platform_authentication_scope() @@ -63,18 +68,30 @@ async def add_tool_servers_to_agent( auth_token = auth_token_obj.token self._logger.info(f"Listing MCP tool servers for agent {agentic_app_id}") + + options = ToolOptions(orchestrator_name=self._orchestrator_name) + + # Pass auth context for V2 per-audience token acquisition (production path). + # In dev/Playground mode (empty auth_handler_name), the SDK reads per-server + # tokens from BEARER_TOKEN_MCP_ / BEARER_TOKEN env vars automatically + # via its internal _attach_dev_tokens method. + list_kwargs = {} + if auth_handler_name: + list_kwargs = { + "authorization": auth, + "auth_handler_name": auth_handler_name, + "turn_context": context, + } + mcp_server_configs = await self.config_service.list_tool_servers( agentic_app_id=agentic_app_id, - auth_token=auth_token or "", + auth_token=auth_token, + options=options, + **list_kwargs, ) self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations") - # Base headers used as fallback when no per-server headers are provided (V1) - base_headers = { - "Authorization": f"Bearer {auth_token}" - } - # Convert MCP server configs to McpToolset objects mcp_servers_info = [] @@ -86,22 +103,20 @@ async def add_tool_servers_to_agent( ) continue - # V2: look up per-server token from env (BEARER_TOKEN_MCP_{SERVERNAME_UPPER}) - # e.g. mcp_CalendarTools → BEARER_TOKEN_MCP_CALENDARTOOLS - env_key = f"BEARER_TOKEN_{server_config.mcp_server_unique_name.upper()}" - per_server_token = os.getenv(env_key) - if per_server_token: - mcp_server_headers = {"Authorization": f"Bearer {per_server_token}"} - else: - # Fall back: merge base headers with any server_config.headers (V1 path) - server_level_headers = getattr(server_config, "headers", None) or {} - mcp_server_headers = {**base_headers, **server_level_headers} - - server_url = getattr(server_config, "url", None) or server_config.mcp_server_unique_name + # server_config.headers already contains the per-audience Authorization token: + # - Dev mode: set by SDK's _attach_dev_tokens (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) + # - Prod mode: set by SDK's _attach_per_audience_tokens (per-audience OAuth exchange) + base_headers = { + Constants.Headers.USER_AGENT: Utility.get_user_agent_header( + self._orchestrator_name + ) + } + server_level_headers = dict(server_config.headers) if server_config.headers else {} + mcp_server_headers = {**base_headers, **server_level_headers} server_info = McpToolset( connection_params=StreamableHTTPConnectionParams( - url=server_url, + url=server_config.url, headers=mcp_server_headers, timeout=30.0, ) diff --git a/python/google-adk/sample-agent/pyproject.toml b/python/google-adk/sample-agent/pyproject.toml index a95436f2..3f32f5f5 100644 --- a/python/google-adk/sample-agent/pyproject.toml +++ b/python/google-adk/sample-agent/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ # Microsoft Agent 365 SDK packages "microsoft_agents_a365_tooling >= 0.1.0", + "microsoft_agents_a365_tooling_extensions_googleadk >= 0.1.0", "microsoft_agents_a365_observability_core >= 0.1.0", "microsoft_agents_a365_notifications >= 0.1.0", ] From 87893a7f615d8518d94b65cfe56015f0a3003cb2 Mon Sep 17 00:00:00 2001 From: biswapm Date: Sun, 12 Apr 2026 15:51:25 +0530 Subject: [PATCH 06/10] Add V2 per-audience MCP token support across all Python samples - Fix CrewAI legacy list_tool_servers() to pass authorization, auth_handler_name, and turn_context to the SDK so V2 per-audience tokens are acquired correctly - Document BEARER_TOKEN_MCP_ dev env vars in all five .env.template files (openai, agent-framework, claude, crewai, google-adk) for V2 blueprint local testing --- python/agent-framework/sample-agent/.env.template | 8 ++++++++ python/claude/sample-agent/.env.template | 8 ++++++++ python/crewai/sample_agent/.env.template | 8 ++++++++ .../sample_agent/mcp_tool_registration_service.py | 13 ++++++++++++- python/google-adk/sample-agent/.env.template | 8 ++++++++ python/openai/sample-agent/.env.template | 8 ++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index 1ed030d8..069f6140 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -77,6 +77,14 @@ AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default # Hostname for a locally running custom MCP server (leave blank if not using one) MCP_SERVER_HOST= +# --- V2 Per-Server Bearer Tokens (development only) --- +# For V2 MCP blueprints, each server can have its own scoped token. +# The SDK reads BEARER_TOKEN_MCP_ for each discovered server, +# falling back to BEARER_TOKEN if the per-server variable is not set. +# Example — for a server registered as "MailTools": +# BEARER_TOKEN_MCP_MAILTOOLS=<> +# Generate tokens with: a365 develop get-token -o raw + # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index 9f0a0697..afa54bea 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -137,6 +137,14 @@ ENVIRONMENT=Development # Hostname for a locally running custom MCP server (leave blank if not using one) MCP_SERVER_HOST= +# --- V2 Per-Server Bearer Tokens (development only) --- +# For V2 MCP blueprints, each server can have its own scoped token. +# The SDK reads BEARER_TOKEN_MCP_ for each discovered server, +# falling back to BEARER_TOKEN if the per-server variable is not set. +# Example — for a server registered as "MailTools": +# BEARER_TOKEN_MCP_MAILTOOLS=<> +# Generate tokens with: a365 develop get-token -o raw + # ============================================================================= # SERVER CONFIGURATION # ============================================================================= diff --git a/python/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index 407fb80a..ea776c56 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -151,6 +151,14 @@ MCP_SERVER_PORT= # Hostname for a locally running custom MCP server (optional) MCP_SERVER_HOST= +# --- V2 Per-Server Bearer Tokens (development only) --- +# For V2 MCP blueprints, each server can have its own scoped token. +# The SDK reads BEARER_TOKEN_MCP_ for each discovered server, +# falling back to BEARER_TOKEN if the per-server variable is not set. +# Example — for a server registered as "MailTools": +# BEARER_TOKEN_MCP_MAILTOOLS=<> +# Generate tokens with: a365 develop get-token -o raw + # ============================================================================= # SERVER CONFIGURATION # ============================================================================= diff --git a/python/crewai/sample_agent/mcp_tool_registration_service.py b/python/crewai/sample_agent/mcp_tool_registration_service.py index 8cc7cd0e..1e7d62b3 100644 --- a/python/crewai/sample_agent/mcp_tool_registration_service.py +++ b/python/crewai/sample_agent/mcp_tool_registration_service.py @@ -707,7 +707,7 @@ async def list_tool_servers( ) -> List: """ Fetch MCP server configurations the agent is allowed to use. - + This is a legacy method for backwards compatibility. Prefer using discover_and_connect_servers() for full functionality. @@ -720,9 +720,20 @@ async def list_tool_servers( token = auth_token_obj.token self._logger.info("Listing MCP tool servers for agent %s", agentic_app_id) + + # Pass auth context for V2 per-audience token acquisition. + list_kwargs = {} + if auth_handler_name: + list_kwargs = { + "authorization": auth, + "auth_handler_name": auth_handler_name, + "turn_context": context, + } + mcp_server_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, auth_token=token, + **list_kwargs, ) self._logger.info("Loaded %d MCP server configurations", len(mcp_server_configs)) diff --git a/python/google-adk/sample-agent/.env.template b/python/google-adk/sample-agent/.env.template index 4d99fdd6..2cf3256b 100644 --- a/python/google-adk/sample-agent/.env.template +++ b/python/google-adk/sample-agent/.env.template @@ -58,6 +58,14 @@ AGENTIC_TENANT_ID=<> # When BEARER_TOKEN is set, agentic auth is bypassed. BEARER_TOKEN= +# --- V2 Per-Server Bearer Tokens (development only) --- +# For V2 MCP blueprints, each server can have its own scoped token. +# The SDK reads BEARER_TOKEN_MCP_ for each discovered server, +# falling back to BEARER_TOKEN if the per-server variable is not set. +# Example — for a server registered as "MailTools": +# BEARER_TOKEN_MCP_MAILTOOLS=<> +# Generate tokens with: a365 develop get-token -o raw + # --- Option B: Agentic Authentication (production / Teams deployment) --- # Authentication handler: # "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index 39f33a5e..2a972941 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -82,6 +82,14 @@ MCP_SERVER_PORT=8000 # Hostname for a locally running custom MCP server (optional) MCP_SERVER_HOST=localhost +# --- V2 Per-Server Bearer Tokens (development only) --- +# For V2 MCP blueprints, each server can have its own scoped token. +# The SDK reads BEARER_TOKEN_MCP_ for each discovered server, +# falling back to BEARER_TOKEN if the per-server variable is not set. +# Example — for a server registered as "MailTools": +# BEARER_TOKEN_MCP_MAILTOOLS=<> +# Generate tokens with: a365 develop get-token -o raw + # ============================================================================= # AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) # ============================================================================= From 129190716541f52ce0a13ec1173cf623507d17fa Mon Sep 17 00:00:00 2001 From: biswapm Date: Sun, 12 Apr 2026 18:48:06 +0530 Subject: [PATCH 07/10] removed port clean code --- python/google-adk/sample-agent/main.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/python/google-adk/sample-agent/main.py b/python/google-adk/sample-agent/main.py index 88c62db5..16c27afc 100644 --- a/python/google-adk/sample-agent/main.py +++ b/python/google-adk/sample-agent/main.py @@ -136,23 +136,6 @@ async def health_check(req: Request) -> Response: port = 3978 logger.info("PORT not set, using default: %d", port) - # Free the port if already in use — prevents [Errno 10048] on Windows restart - import subprocess as _sp, sys as _sys - if _sys.platform == "win32": - try: - _out = _sp.check_output( - f'netstat -ano', shell=True, text=True, stderr=_sp.DEVNULL - ) - for _line in _out.splitlines(): - if f":{port} " in _line and "LISTENING" in _line: - _pid = _line.split()[-1] - if _pid.isdigit(): - _sp.run(f"taskkill /PID {_pid} /F", - shell=True, capture_output=True) - logger.info("Released port %d (killed PID %s)", port, _pid) - except Exception: - pass - logger.info("Listening on %s:%d/api/messages", host, port) run_app(app, host=host, port=port, handle_signals=True) except KeyboardInterrupt: From b8a5c9519520dd880c3a1ca132a6b2f38cd71fee Mon Sep 17 00:00:00 2001 From: biswapm Date: Wed, 15 Apr 2026 13:15:00 +0530 Subject: [PATCH 08/10] Changes to support MCP V2 --- .../sample-agent/ToolingManifest.json | 8 ++-- python/google-adk/sample-agent/agent.py | 45 +++++++++++++------ .../mcp_tool_registration_service.py | 12 ++++- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/python/google-adk/sample-agent/ToolingManifest.json b/python/google-adk/sample-agent/ToolingManifest.json index c051e793..496a3a58 100644 --- a/python/google-adk/sample-agent/ToolingManifest.json +++ b/python/google-adk/sample-agent/ToolingManifest.json @@ -3,17 +3,17 @@ { "mcpServerName": "mcp_CalendarTools", "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", "publisher": "Microsoft" } ] diff --git a/python/google-adk/sample-agent/agent.py b/python/google-adk/sample-agent/agent.py index 66ed1906..b9aff05a 100644 --- a/python/google-adk/sample-agent/agent.py +++ b/python/google-adk/sample-agent/agent.py @@ -183,24 +183,43 @@ async def _cleanup_agent(self, agent: Agent): if hasattr(tool, "close"): await tool.close() + @staticmethod + def _check_jwt_expiry(token: str, name: str) -> bool: + """Returns True if token is valid (not expired), False if expired. Logs a warning if expired.""" + try: + from base64 import urlsafe_b64decode + import json as _json + payload = token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logger.warning( + "%s is expired (exp=%d) — regenerate with `a365 develop get-token` " + "and RESTART the agent to pick up new tokens.", + name, exp, + ) + return False + except Exception: + pass # non-JWT format; treat as valid + return True + async def _initialize_agent(self, agent, auth, auth_handler_name, turn_context): """Initialize the agent with MCP tools and authentication.""" # Validate BEARER_TOKEN — pass empty string if expired so the SDK uses # the proper auth handler instead of a stale token that triggers an OBO hang. bearer_token = os.getenv("BEARER_TOKEN", "") - if bearer_token: - try: - from base64 import urlsafe_b64decode - import json as _json - payload = bearer_token.split(".")[1] - if len(payload) % 4 != 0: - payload += "=" * (4 - len(payload) % 4) - exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) - if exp and time.time() > exp: - logger.warning("BEARER_TOKEN is expired — skipping token, will use auth handler") - bearer_token = "" - except Exception: - pass # non-JWT token format; pass it through as-is + if bearer_token and not self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): + bearer_token = "" + + # Warn about expired per-server MCP tokens in dev mode. + # These are looked up by the SDK as BEARER_TOKEN_MCP_. + # If expired, regenerate with `a365 develop get-token` and restart the agent. + if not auth_handler_name: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_MCP_")]: + mcp_token = os.environ[env_var] + if mcp_token: + self._check_jwt_expiry(mcp_token, env_var) # Skip MCP init if there's no token and no auth handler — avoids MCP # session errors when running locally/Playground without valid credentials. diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index cc851c02..457ae388 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -104,8 +104,8 @@ async def add_tool_servers_to_agent( continue # server_config.headers already contains the per-audience Authorization token: - # - Dev mode: set by SDK's _attach_dev_tokens (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) - # - Prod mode: set by SDK's _attach_per_audience_tokens (per-audience OAuth exchange) + # - Dev mode: set by SDK's _create_dev_token_acquirer (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) + # - Prod mode: set by SDK's _create_obo_token_acquirer (per-audience OBO exchange) base_headers = { Constants.Headers.USER_AGENT: Utility.get_user_agent_header( self._orchestrator_name @@ -114,6 +114,14 @@ async def add_tool_servers_to_agent( server_level_headers = dict(server_config.headers) if server_config.headers else {} mcp_server_headers = {**base_headers, **server_level_headers} + has_auth = Constants.Headers.AUTHORIZATION in mcp_server_headers + self._logger.info( + "Configuring MCP server '%s' → %s (auth_header=%s)", + server_config.mcp_server_name, + server_config.url, + "present" if has_auth else "MISSING — MCP calls will fail without a valid token", + ) + server_info = McpToolset( connection_params=StreamableHTTPConnectionParams( url=server_config.url, From e806f1eb4f31efb008cbc05aaa518baacfdd1924 Mon Sep 17 00:00:00 2001 From: biswapm Date: Wed, 15 Apr 2026 17:40:29 +0530 Subject: [PATCH 09/10] MCP V1/V2 consistency across all Python samples - Add _check_jwt_expiry to agent-framework, openai, claude, crewai samples with warning + token discard when BEARER_TOKEN or per-server tokens expire - Use generic BEARER_TOKEN_ prefix filter (not BEARER_TOKEN_MCP_ only) so non-MCP-prefixed server names are supported in all 5 samples - Switch ENVIRONMENT -> PYTHON_ENVIRONMENT in openai and claude samples - Default USE_AGENTIC_AUTH=false and AUTH_HANDLER_NAME= (empty) in all .env.template files for local-dev-first consistency with google-adk - Update ToolingManifest.json to V2 format (Tools.ListInvoke.All + per-server audience GUIDs) in agent-framework, claude, crewai, openai --- .../sample-agent/.env.template | 4 +- .../sample-agent/ToolingManifest.json | 11 +++-- python/agent-framework/sample-agent/agent.py | 36 +++++++++++++++- python/claude/sample-agent/.env.template | 7 +--- .../claude/sample-agent/ToolingManifest.json | 8 ++-- python/claude/sample-agent/agent.py | 2 +- .../mcp_tool_registration_service.py | 40 ++++++++++++++++-- python/crewai/sample_agent/.env.template | 2 +- .../crewai/sample_agent/ToolingManifest.json | 8 ++-- python/crewai/sample_agent/agent.py | 2 +- .../mcp_tool_registration_service.py | 32 +++++++++++++- python/google-adk/sample-agent/agent.py | 6 +-- python/google-adk/sample-agent/main.py | 2 +- .../mcp_tool_registration_service.py | 2 +- python/openai/sample-agent/.env.template | 2 +- .../openai/sample-agent/ToolingManifest.json | 8 ++-- python/openai/sample-agent/agent.py | 42 +++++++++++++++++-- 17 files changed, 170 insertions(+), 44 deletions(-) diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index 069f6140..dcfc828d 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -60,12 +60,12 @@ BEARER_TOKEN=<> # --- Option B: Agentic Authentication (production / Teams deployment) --- # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. -USE_AGENTIC_AUTH=true +USE_AGENTIC_AUTH=false # Authentication handler: # "AGENTIC" — production (Teams / Azure/GCP/AWS deployment). Enforces agentic auth on message handlers. # "" — local dev / Agents Playground. Allows anonymous access. -AUTH_HANDLER_NAME=AGENTIC +AUTH_HANDLER_NAME= # OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default diff --git a/python/agent-framework/sample-agent/ToolingManifest.json b/python/agent-framework/sample-agent/ToolingManifest.json index 78cef4b3..bae107fb 100644 --- a/python/agent-framework/sample-agent/ToolingManifest.json +++ b/python/agent-framework/sample-agent/ToolingManifest.json @@ -1,21 +1,20 @@ { "mcpServers": [ { - "mcpServerName": "mcp_CalendarTools", "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", "publisher": "Microsoft" } ] -} \ No newline at end of file +} diff --git a/python/agent-framework/sample-agent/agent.py b/python/agent-framework/sample-agent/agent.py index 8f9fcb7b..86cbd71a 100644 --- a/python/agent-framework/sample-agent/agent.py +++ b/python/agent-framework/sample-agent/agent.py @@ -20,6 +20,7 @@ import asyncio import logging import os +import time from typing import Optional from dotenv import load_dotenv @@ -196,6 +197,27 @@ def _enable_agentframework_instrumentation(self): # ========================================================================= # + @staticmethod + def _check_jwt_expiry(token: str, name: str) -> bool: + """Returns True if token is valid (not expired), False if expired. Logs a warning if expired.""" + try: + from base64 import urlsafe_b64decode + import json as _json + payload = token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logger.warning( + "%s is expired (exp=%d) — regenerate with `a365 develop get-token` " + "and RESTART the agent to pick up new tokens.", + name, exp, + ) + return False + except Exception: + pass # non-JWT format; treat as valid + return True + def _initialize_services(self): """Initialize MCP services""" try: @@ -218,6 +240,18 @@ async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: Option agent_instructions = instructions or self.AGENT_PROMPT use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true" + # Validate bearer token — clear if expired to avoid silent 401s. + bearer_token = self.auth_options.bearer_token + if bearer_token and not self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): + bearer_token = "" + + # Warn about expired per-server tokens in dev mode. + if not use_agentic_auth and not auth_handler_name: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_") and k != "BEARER_TOKEN"]: + mcp_token = os.environ[env_var] + if mcp_token: + self._check_jwt_expiry(mcp_token, env_var) + if use_agentic_auth: self.agent = await self.tool_service.add_tool_servers_to_agent( chat_client=self.chat_client, @@ -234,7 +268,7 @@ async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: Option initial_tools=[], auth=auth, auth_handler_name=auth_handler_name, - auth_token=self.auth_options.bearer_token, + auth_token=bearer_token, turn_context=context, ) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index afa54bea..ad685a4c 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -57,12 +57,12 @@ BEARER_TOKEN=<> # --- Option B: Agentic Authentication (production / Teams deployment) --- # Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. -USE_AGENTIC_AUTH=true +USE_AGENTIC_AUTH=false # Authentication handler: # "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. # "" — local dev / Agents Playground. Allows anonymous access. -AUTH_HANDLER_NAME=AGENTIC +AUTH_HANDLER_NAME= # OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default @@ -131,9 +131,6 @@ WEBSITE_INSTANCE_ID= # MCP (MODEL CONTEXT PROTOCOL) — ADVANCED # ============================================================================= -# Label shown in logs/traces to identify the environment (informational only) -ENVIRONMENT=Development - # Hostname for a locally running custom MCP server (leave blank if not using one) MCP_SERVER_HOST= diff --git a/python/claude/sample-agent/ToolingManifest.json b/python/claude/sample-agent/ToolingManifest.json index 2ccf1029..bae107fb 100644 --- a/python/claude/sample-agent/ToolingManifest.json +++ b/python/claude/sample-agent/ToolingManifest.json @@ -3,17 +3,17 @@ { "mcpServerName": "mcp_CalendarTools", "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", "publisher": "Microsoft" } ] diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index a09e76f6..43b19c3d 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -217,7 +217,7 @@ async def setup_mcp_servers( # Get auth token - prefer token exchange for proper MCP authentication # When USE_AGENTIC_AUTH=true, the service will exchange token with proper scopes # Otherwise, we fall back to the static bearer token (for local dev) - use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "true").lower() == "true" + use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true" auth_token = None if not use_agentic_auth: diff --git a/python/claude/sample-agent/mcp_tool_registration_service.py b/python/claude/sample-agent/mcp_tool_registration_service.py index 6afdc34e..51e2cfdd 100644 --- a/python/claude/sample-agent/mcp_tool_registration_service.py +++ b/python/claude/sample-agent/mcp_tool_registration_service.py @@ -19,6 +19,7 @@ from dataclasses import dataclass, field import logging import os +import time import aiohttp import asyncio @@ -100,10 +101,31 @@ def __init__(self, logger: Optional[logging.Logger] = None): self._auth_token: Optional[str] = None self._config_service = McpToolServerConfigurationService(logger=self._logger) + @staticmethod + def _check_jwt_expiry(token: str, name: str) -> bool: + """Returns True if token is valid (not expired), False if expired. Logs a warning if expired.""" + try: + from base64 import urlsafe_b64decode + import json as _json + payload = token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logging.getLogger(__name__).warning( + "%s is expired (exp=%d) — regenerate with `a365 develop get-token` " + "and RESTART the agent to pick up new tokens.", + name, exp, + ) + return False + except Exception: + pass # non-JWT format; treat as valid + return True + def _load_manifest_servers_fallback(self) -> List[Dict[str, Any]]: """ Load MCP server configurations directly from ToolingManifest.json. - + This is a fallback for local development when McpToolServerConfigurationService cannot discover servers (e.g., no Gateway connection). @@ -214,16 +236,28 @@ async def discover_and_connect_servers( self._auth_token = auth_token self._logger.info("Using provided auth token for MCP authentication") else: - environment = os.getenv("ENVIRONMENT", "Production").strip().lower() + environment = os.getenv("PYTHON_ENVIRONMENT", "Production").strip().lower() bearer_token = (os.getenv("BEARER_TOKEN") or "").strip() if bearer_token: # Bearer token mode (development only) if environment != "development": raise ValueError( - "BEARER_TOKEN is set but ENVIRONMENT is not 'development'. " + "BEARER_TOKEN is set but PYTHON_ENVIRONMENT is not 'development'. " "Bearer tokens are only supported in development environments." ) + # Clear if expired — fall through to auth_handler or bare mode. + if not self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): + bearer_token = "" + + # Warn about expired per-server tokens in dev mode. + if not auth_handler_name: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_") and k != "BEARER_TOKEN"]: + mcp_token = os.environ[env_var] + if mcp_token: + self._check_jwt_expiry(mcp_token, env_var) + + if bearer_token: self._auth_token = bearer_token self._logger.info("Using BEARER_TOKEN authentication (development mode)") elif auth_handler_name: diff --git a/python/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index ea776c56..91eb2c67 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -68,7 +68,7 @@ USE_AGENTIC_AUTH=false # Authentication handler: # "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. # "" — local dev / Agents Playground. Allows anonymous access. -AUTH_HANDLER_NAME=AGENTIC +AUTH_HANDLER_NAME= # OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default diff --git a/python/crewai/sample_agent/ToolingManifest.json b/python/crewai/sample_agent/ToolingManifest.json index 2ccf1029..bae107fb 100644 --- a/python/crewai/sample_agent/ToolingManifest.json +++ b/python/crewai/sample_agent/ToolingManifest.json @@ -3,17 +3,17 @@ { "mcpServerName": "mcp_CalendarTools", "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", "publisher": "Microsoft" } ] diff --git a/python/crewai/sample_agent/agent.py b/python/crewai/sample_agent/agent.py index c296680a..391a88a3 100644 --- a/python/crewai/sample_agent/agent.py +++ b/python/crewai/sample_agent/agent.py @@ -103,7 +103,7 @@ async def _setup_mcp_servers( agentic_app_id = os.getenv("AGENTIC_APP_ID", DEFAULT_AGENT_ID) # Get auth token - prefer token exchange for proper MCP authentication - use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "true").lower() == "true" + use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true" auth_token = None if not use_agentic_auth: diff --git a/python/crewai/sample_agent/mcp_tool_registration_service.py b/python/crewai/sample_agent/mcp_tool_registration_service.py index 1e7d62b3..2e368c75 100644 --- a/python/crewai/sample_agent/mcp_tool_registration_service.py +++ b/python/crewai/sample_agent/mcp_tool_registration_service.py @@ -21,6 +21,7 @@ import logging import os import random +import time import aiohttp import asyncio import json @@ -98,6 +99,26 @@ def __init__(self, logger: Optional[logging.Logger] = None): self._auth_token: Optional[str] = None self._config_service = McpToolServerConfigurationService(logger=self._logger) + @staticmethod + def _check_jwt_expiry(token: str, name: str) -> bool: + """Returns True if token is valid (not expired), False if expired. Logs a warning if expired.""" + try: + from base64 import urlsafe_b64decode + payload = token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logging.getLogger(__name__).warning( + "%s is expired (exp=%d) — regenerate with `a365 develop get-token` " + "and RESTART the agent to pick up new tokens.", + name, exp, + ) + return False + except Exception: + pass # non-JWT format; treat as valid + return True + def _load_manifest_servers_fallback(self) -> List[Dict[str, Any]]: """ Load MCP server configurations directly from ToolingManifest.json. @@ -222,10 +243,17 @@ async def discover_and_connect_servers( # Fallback to static BEARER_TOKEN from environment if not auth_token: bearer_token = os.getenv("BEARER_TOKEN", "").strip() - if bearer_token: + if bearer_token and self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): auth_token = bearer_token self._logger.info("â„šī¸ Using BEARER_TOKEN from environment for MCP authentication") - + + # Warn about expired per-server tokens in dev mode. + if not auth_handler_name: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_") and k != "BEARER_TOKEN"]: + mcp_token = os.environ[env_var] + if mcp_token: + self._check_jwt_expiry(mcp_token, env_var) + # For local development, allow connections without auth token if not auth_token: self._logger.info("â„šī¸ No auth token - will attempt local connections only") diff --git a/python/google-adk/sample-agent/agent.py b/python/google-adk/sample-agent/agent.py index b9aff05a..4cfd3e30 100644 --- a/python/google-adk/sample-agent/agent.py +++ b/python/google-adk/sample-agent/agent.py @@ -212,11 +212,11 @@ async def _initialize_agent(self, agent, auth, auth_handler_name, turn_context): if bearer_token and not self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): bearer_token = "" - # Warn about expired per-server MCP tokens in dev mode. - # These are looked up by the SDK as BEARER_TOKEN_MCP_. + # Warn about expired per-server tokens in dev mode. + # These are looked up by the SDK as BEARER_TOKEN_. # If expired, regenerate with `a365 develop get-token` and restart the agent. if not auth_handler_name: - for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_MCP_")]: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_") and k != "BEARER_TOKEN"]: mcp_token = os.environ[env_var] if mcp_token: self._check_jwt_expiry(mcp_token, env_var) diff --git a/python/google-adk/sample-agent/main.py b/python/google-adk/sample-agent/main.py index 16c27afc..7ac7f853 100644 --- a/python/google-adk/sample-agent/main.py +++ b/python/google-adk/sample-agent/main.py @@ -41,7 +41,7 @@ def start_server(agent_app: AgentApplication): isProduction = ( os.getenv("WEBSITE_SITE_NAME") is not None # Azure App Service or os.getenv("K_SERVICE") is not None # GCP Cloud Run - or os.getenv("ENVIRONMENT", "").lower() == "production" # Explicit flag + or os.getenv("PYTHON_ENVIRONMENT", "").lower() == "production" # Explicit flag ) async def entry_point(req: Request) -> Response: diff --git a/python/google-adk/sample-agent/mcp_tool_registration_service.py b/python/google-adk/sample-agent/mcp_tool_registration_service.py index 457ae388..b32844e0 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -104,7 +104,7 @@ async def add_tool_servers_to_agent( continue # server_config.headers already contains the per-audience Authorization token: - # - Dev mode: set by SDK's _create_dev_token_acquirer (reads BEARER_TOKEN_MCP_* / BEARER_TOKEN) + # - Dev mode: set by SDK's _create_dev_token_acquirer (reads BEARER_TOKEN_* / BEARER_TOKEN) # - Prod mode: set by SDK's _create_obo_token_acquirer (per-audience OBO exchange) base_headers = { Constants.Headers.USER_AGENT: Utility.get_user_agent_header( diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index 2a972941..a4072efe 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -64,7 +64,7 @@ USE_AGENTIC_AUTH=false # Authentication handler: # "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. # "" — local dev / Agents Playground. Allows anonymous access. -AUTH_HANDLER_NAME=AGENTIC +AUTH_HANDLER_NAME= # OAuth scope for the initial blueprint token exchange with the Agent 365 platform AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default diff --git a/python/openai/sample-agent/ToolingManifest.json b/python/openai/sample-agent/ToolingManifest.json index 2ccf1029..bae107fb 100644 --- a/python/openai/sample-agent/ToolingManifest.json +++ b/python/openai/sample-agent/ToolingManifest.json @@ -3,17 +3,17 @@ { "mcpServerName": "mcp_CalendarTools", "mcpServerUniqueName": "mcp_CalendarTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", "scope": "Tools.ListInvoke.All", - "audience": "19ec8e8a-5f2f-4e00-9f66-d3e5b4c3e201", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", "publisher": "Microsoft" }, { "mcpServerName": "mcp_MailTools", "mcpServerUniqueName": "mcp_MailTools", - "url": "https://test.agent365.svc.cloud.dev.microsoft/agents/servers/mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", "scope": "Tools.ListInvoke.All", - "audience": "24b71c94-d291-44af-ac1e-a396e6837fd3", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", "publisher": "Microsoft" } ] diff --git a/python/openai/sample-agent/agent.py b/python/openai/sample-agent/agent.py index 412b5de3..1df649f3 100644 --- a/python/openai/sample-agent/agent.py +++ b/python/openai/sample-agent/agent.py @@ -19,6 +19,7 @@ import dataclasses import logging import os +import time from agent_interface import AgentInterface from dotenv import load_dotenv @@ -72,13 +73,34 @@ class OpenAIAgentWithMCP(AgentInterface): # ========================================================================= # + @staticmethod + def _check_jwt_expiry(token: str, name: str) -> bool: + """Returns True if token is valid (not expired), False if expired. Logs a warning if expired.""" + try: + from base64 import urlsafe_b64decode + import json as _json + payload = token.split(".")[1] + if len(payload) % 4 != 0: + payload += "=" * (4 - len(payload) % 4) + exp = _json.loads(urlsafe_b64decode(payload)).get("exp", 0) + if exp and time.time() > exp: + logger.warning( + "%s is expired (exp=%d) — regenerate with `a365 develop get-token` " + "and RESTART the agent to pick up new tokens.", + name, exp, + ) + return False + except Exception: + pass # non-JWT format; treat as valid + return True + @staticmethod def should_skip_tooling_on_errors() -> bool: """ Checks if graceful fallback to bare LLM mode is enabled when MCP tools fail to load. This is only allowed in Development environment AND when SKIP_TOOLING_ON_ERRORS is explicitly set to "true". """ - environment = os.getenv("ENVIRONMENT", os.getenv("ASPNETCORE_ENVIRONMENT", "Production")) + environment = os.getenv("PYTHON_ENVIRONMENT", os.getenv("ASPNETCORE_ENVIRONMENT", "Production")) skip_tooling_on_errors = os.getenv("SKIP_TOOLING_ON_ERRORS", "").lower() # Only allow skipping tooling errors in Development mode AND when explicitly enabled @@ -260,7 +282,19 @@ async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: str, c try: # Check if agentic auth is enabled use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true" - + + # Validate bearer token — clear if expired to avoid silent 401s. + bearer_token = self.auth_options.bearer_token + if bearer_token and not self._check_jwt_expiry(bearer_token, "BEARER_TOKEN"): + bearer_token = "" + + # Warn about expired per-server tokens in dev mode. + if not use_agentic_auth and not auth_handler_name: + for env_var in [k for k in os.environ if k.startswith("BEARER_TOKEN_") and k != "BEARER_TOKEN"]: + mcp_token = os.environ[env_var] + if mcp_token: + self._check_jwt_expiry(mcp_token, env_var) + # Priority 1: Agentic auth enabled (production/Teams authentication) # When USE_AGENTIC_AUTH=true, always use agentic auth - never fall back to bearer token if use_agentic_auth: @@ -275,14 +309,14 @@ async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: str, c context=context, ) # Priority 2: Bearer token provided in config (for local dev/testing when agentic auth is disabled) - elif self.auth_options.bearer_token: + elif bearer_token: logger.info("🔑 Using bearer token from config for MCP servers (USE_AGENTIC_AUTH=false)") self.agent = await self.tool_service.add_tool_servers_to_agent( agent=self.agent, auth=auth, auth_handler_name=auth_handler_name, context=context, - auth_token=self.auth_options.bearer_token, + auth_token=bearer_token, ) # Priority 3: Auth handler configured without USE_AGENTIC_AUTH flag elif auth_handler_name: From 7172e1c708f06f1d7828daa000d4d3cf72999ff2 Mon Sep 17 00:00:00 2001 From: biswapm Date: Wed, 15 Apr 2026 17:45:41 +0530 Subject: [PATCH 10/10] Deleted test --- python/crewai/sample_agent/tests/__init__.py | 2 - python/crewai/sample_agent/tests/conftest.py | 41 --- .../test_mcp_tool_registration_service.py | 266 ------------------ 3 files changed, 309 deletions(-) delete mode 100644 python/crewai/sample_agent/tests/__init__.py delete mode 100644 python/crewai/sample_agent/tests/conftest.py delete mode 100644 python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py diff --git a/python/crewai/sample_agent/tests/__init__.py b/python/crewai/sample_agent/tests/__init__.py deleted file mode 100644 index 59e481eb..00000000 --- a/python/crewai/sample_agent/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. diff --git a/python/crewai/sample_agent/tests/conftest.py b/python/crewai/sample_agent/tests/conftest.py deleted file mode 100644 index 80113008..00000000 --- a/python/crewai/sample_agent/tests/conftest.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Pytest configuration: mock Microsoft SDK imports that may not be installed -in the test environment so unit tests can import the service modules directly. -""" - -import sys -from unittest.mock import MagicMock - - -def _mock_sdk(): - mocks = { - "microsoft_agents": MagicMock(), - "microsoft_agents.hosting": MagicMock(), - "microsoft_agents.hosting.core": MagicMock(), - "microsoft_agents_a365": MagicMock(), - "microsoft_agents_a365.tooling": MagicMock(), - "microsoft_agents_a365.tooling.utils": MagicMock(), - "microsoft_agents_a365.tooling.utils.constants": MagicMock(), - "microsoft_agents_a365.tooling.utils.utility": MagicMock(), - "microsoft_agents_a365.tooling.services": MagicMock(), - "microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service": MagicMock(), - } - - headers_mock = MagicMock() - headers_mock.AUTHORIZATION = "Authorization" - headers_mock.BEARER_PREFIX = "Bearer" - mocks["microsoft_agents_a365.tooling.utils.constants"].Constants.Headers = headers_mock - - mocks["microsoft_agents_a365.tooling.utils.utility"].get_mcp_platform_authentication_scope = ( - lambda: ["https://api.powerplatform.com/.default"] - ) - - for name, mock in mocks.items(): - if name not in sys.modules: - sys.modules[name] = mock - - -_mock_sdk() diff --git a/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py b/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py deleted file mode 100644 index 0d6f2a1c..00000000 --- a/python/crewai/sample_agent/tests/test_mcp_tool_registration_service.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -Unit tests for CrewAI MCP Tool Registration Service. - -Covers V2 MCP changes: -- authorization_context passed to list_tool_servers -- publisher and headers extracted from SDK configs and ToolingManifest.json -- Per-server header merging: {**base_headers, **server_headers} -""" - -import json -import os -import sys -import pytest -from unittest.mock import AsyncMock, MagicMock, patch - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from mcp_tool_registration_service import McpToolRegistrationService - - -# --------------------------------------------------------------------------- -# _build_full_url -# --------------------------------------------------------------------------- - -class TestBuildFullUrl: - def setup_method(self): - self.service = McpToolRegistrationService() - - def test_full_url_unchanged(self): - url = "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Test" - assert self.service._build_full_url(url) == url - - def test_bare_server_name_becomes_agents_servers_path(self): - with patch.dict(os.environ, {"MCP_PLATFORM_ENDPOINT": "https://ep.com"}): - assert self.service._build_full_url("mcp_Test") == "https://ep.com/agents/servers/mcp_Test" - - def test_empty_returns_empty(self): - assert self.service._build_full_url("") == "" - - -# --------------------------------------------------------------------------- -# _load_manifest_servers_fallback -# --------------------------------------------------------------------------- - -class TestLoadManifestServersFallback: - def setup_method(self): - self.service = McpToolRegistrationService() - - def test_loads_publisher_and_headers(self, tmp_path): - manifest = {"mcpServers": [{ - "mcpServerName": "mcp_Test", - "url": "https://example.com/mcp_Test", - "scope": "McpServers.Test.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft", - "headers": {"X-Custom": "val"}, - }]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - - assert servers[0]["publisher"] == "Microsoft" - assert servers[0]["headers"] == {"X-Custom": "val"} - - def test_skips_servers_without_url(self, tmp_path): - manifest = {"mcpServers": [{"mcpServerName": "mcp_NoUrl"}]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - with patch("os.getcwd", return_value=str(tmp_path)): - assert self.service._load_manifest_servers_fallback() == [] - - def test_returns_empty_when_no_manifest(self, tmp_path): - with patch("os.getcwd", return_value=str(tmp_path)): - assert self.service._load_manifest_servers_fallback() == [] - - def test_defaults_missing_v2_fields(self, tmp_path): - manifest = {"mcpServers": [{"mcpServerName": "mcp_Min", "url": "https://example.com/mcp_Min"}]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - with patch("os.getcwd", return_value=str(tmp_path)): - servers = self.service._load_manifest_servers_fallback() - assert servers[0]["publisher"] == "" - assert servers[0]["headers"] == {} - - -# --------------------------------------------------------------------------- -# _connect_to_server -# --------------------------------------------------------------------------- - -class TestConnectToServer: - def setup_method(self): - self.service = McpToolRegistrationService() - - @pytest.mark.asyncio - async def test_v2_server_headers_override_base_token(self): - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token="base-token", - server_headers={"Authorization": "Bearer per-audience-token"}, - ) - - assert captured["headers"]["Authorization"] == "Bearer per-audience-token" - - @pytest.mark.asyncio - async def test_base_token_used_without_server_headers(self): - captured = {} - - async def mock_list(url, headers, name): - captured["headers"] = dict(headers) - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token="base-token", - server_headers={}, - ) - - assert "Bearer base-token" in captured["headers"].get("Authorization", "") - - @pytest.mark.asyncio - async def test_remote_skipped_with_no_auth(self): - result = await self.service._connect_to_server( - name="mcp_Test", - url="https://example.com/server", - auth_token=None, - server_headers={}, - ) - assert result is None - - @pytest.mark.asyncio - async def test_local_server_no_auth_required(self): - async def mock_list(url, headers, name): - return [] - - with patch.object(self.service, "_list_server_tools", side_effect=mock_list): - result = await self.service._connect_to_server( - name="local", - url="http://127.0.0.1:8080/mcp", - auth_token=None, - server_headers={}, - ) - - assert result is not None - assert result.connected is True - - -# --------------------------------------------------------------------------- -# discover_and_connect_servers -# --------------------------------------------------------------------------- - -class TestDiscoverAndConnectServers: - def setup_method(self): - self.service = McpToolRegistrationService() - - @pytest.mark.asyncio - async def test_passes_authorization_context_to_sdk(self): - mock_auth = MagicMock() - mock_context = MagicMock() - captured = {} - - async def mock_list(**kwargs): - captured.update(kwargs) - return [] - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - ctx = captured.get("authorization_context", {}) - assert ctx.get("auth") is mock_auth - assert ctx.get("auth_handler_name") == "AGENTIC" - - @pytest.mark.asyncio - async def test_per_server_headers_passed_to_connect(self): - mock_auth = MagicMock() - mock_context = MagicMock() - - cfg = MagicMock() - cfg.url = "https://example.com/servers/mcp_Test" - cfg.mcp_server_name = "mcp_Test" - cfg.mcp_server_unique_name = "mcp_Test" - cfg.audience = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" - cfg.scope = "McpServers.Test.All" - cfg.publisher = "Microsoft" - cfg.headers = {"Authorization": "Bearer audience-token"} - - async def mock_list(**kwargs): - return [cfg] - - calls = [] - - async def mock_connect(name, url, auth_token, server_headers=None): - calls.append(server_headers) - conn = MagicMock() - conn.connected = True - conn.tools = [] - conn.url = url - return conn - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=mock_list): - with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert calls[0] == {"Authorization": "Bearer audience-token"} - - @pytest.mark.asyncio - async def test_falls_back_to_manifest_when_sdk_fails(self, tmp_path): - mock_auth = MagicMock() - mock_context = MagicMock() - - manifest = {"mcpServers": [{ - "mcpServerName": "mcp_Fallback", - "url": "http://localhost:9999/mcp", - "scope": "McpServers.Test.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1", - "publisher": "Microsoft", - }]} - (tmp_path / "ToolingManifest.json").write_text(json.dumps(manifest)) - - async def sdk_fails(**kwargs): - raise RuntimeError("SDK unavailable") - - async def mock_connect(name, url, auth_token, server_headers=None): - conn = MagicMock() - conn.connected = True - conn.tools = [] - conn.url = url - conn.name = name - conn.headers = {} - return conn - - with patch.object(self.service._config_service, "list_tool_servers", side_effect=sdk_fails): - with patch.object(self.service, "_connect_to_server", side_effect=mock_connect): - with patch("os.getcwd", return_value=str(tmp_path)): - await self.service.discover_and_connect_servers( - agentic_app_id="test-app", - auth=mock_auth, - auth_handler_name="AGENTIC", - context=mock_context, - auth_token="tok", - ) - - assert len(self.service._connected_servers) == 1 - assert self.service._connected_servers[0].name == "mcp_Fallback"