diff --git a/.gitignore b/.gitignore index 7193dd15..26c87d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,12 @@ publish/ # 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..dcfc828d 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -1,52 +1,165 @@ -# 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. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one) +# ----------------------------------------------------------------------------- + +# --- Option A: Standard OpenAI --- +# Get your API key from https://platform.openai.com/api-keys +OPENAI_API_KEY=<> + +# OpenAI model to use (e.g. gpt-4o, gpt-4o-mini) +OPENAI_MODEL=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=<> # e.g. gpt-4o or gpt-4o-mini +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 Microsoft 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=<> + +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> -# Authentication Handler Configuration -# Set to "AGENTIC" for production agentic auth, or leave empty for no auth handler +# 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 +# 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=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= -# Logging -LOG_LEVEL=INFO +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default -# Observability Configuration -OBSERVABILITY_SERVICE_NAME=agent-framework-sample -OBSERVABILITY_SERVICE_NAMESPACE=agent-framework.samples +# ============================================================================= +# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED +# ============================================================================= + +# 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 -BEARER_TOKEN= -OPENAI_MODEL= +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# 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. -USE_AGENTIC_AUTH=true +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> -# 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= +# 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) +# ============================================================================= + +# 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 -# Required for observability SDK +# 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/agent-framework/sample-agent/ToolingManifest.json b/python/agent-framework/sample-agent/ToolingManifest.json index e842561c..bae107fb 100644 --- a/python/agent-framework/sample-agent/ToolingManifest.json +++ b/python/agent-framework/sample-agent/ToolingManifest.json @@ -1,11 +1,20 @@ { "mcpServers": [ + { + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", + "scope": "Tools.ListInvoke.All", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", + "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" + "scope": "Tools.ListInvoke.All", + "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/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/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index a38cd33e..ad685a4c 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -1,130 +1,181 @@ # ============================================================================= # CLAUDE AGENT SDK 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. +# ============================================================================= -# Anthropic API Key (required) -# Get your API key from: https://console.anthropic.com/ -ANTHROPIC_API_KEY= +# ----------------------------------------------------------------------------- +# ANTHROPIC / CLAUDE API (REQUIRED) +# ----------------------------------------------------------------------------- -# 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 +# Your Anthropic API key — get it 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) +# AGENT IDENTITY (REQUIRED FOR MCP TOOL DISCOVERY) # ============================================================================= -# 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 +# 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=<> -# MCP Server Host -MCP_SERVER_HOST= +# 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=<> -# MCP Platform Endpoint -MCP_PLATFORM_ENDPOINT= +# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> -# ============================================================================= -# MICROSOFT 365 AGENTS SDK CONFIGURATION -# ============================================================================= +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> -# Agent ID (required for agentic authentication) -AGENT_ID=your-agent-id +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> -# Environment ID (optional, defaults to prod) -# Options: dev, test, preprod, prod +# 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 # ============================================================================= -# AUTHENTICATION OPTIONS +# AUTHENTICATION # ============================================================================= -# Use agentic authentication (optional, defaults to false) -# Set to "true" to use agentic authentication with M365 Agents SDK -USE_AGENTIC_AUTH=true +# --- 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=<> -AUTH_HANDLER_NAME=AGENTIC +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. +USE_AGENTIC_AUTH=false -# Bearer token (required if not using client credentials) -# Use for development/testing without full app registration -BEARER_TOKEN= +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. +AUTH_HANDLER_NAME= -# 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) # ============================================================================= +# 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=<> + +# 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 — 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 -# 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= +# 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 -# 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 +# ============================================================================= + +# 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 # ============================================================================= -# 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/claude/sample-agent/ToolingManifest.json b/python/claude/sample-agent/ToolingManifest.json index b8fe9815..bae107fb 100644 --- a/python/claude/sample-agent/ToolingManifest.json +++ b/python/claude/sample-agent/ToolingManifest.json @@ -1,39 +1,20 @@ { "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_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_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_MeServer", - "mcpServerUniqueName": "mcp_MeServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MeServer", - "scope": "McpServers.Me.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "scope": "Tools.ListInvoke.All", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_TeamsServer", - "mcpServerUniqueName": "mcp_TeamsServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_MailTools", + "mcpServerUniqueName": "mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", + "scope": "Tools.ListInvoke.All", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} 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 fb732b54..51e2cfdd 100644 --- a/python/claude/sample-agent/mcp_tool_registration_service.py +++ b/python/claude/sample-agent/mcp_tool_registration_service.py @@ -19,10 +19,12 @@ from dataclasses import dataclass, field import logging import os +import time import aiohttp 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, @@ -99,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). @@ -130,12 +153,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}") @@ -208,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: @@ -249,39 +289,65 @@ async def discover_and_connect_servers( mcp_server_configs = [] try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") + + # 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, - auth_token=auth_token if auth_token else None, + auth_token=auth_token or "", + options=options, + **list_kwargs, ) - + # 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 +367,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 +397,49 @@ 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: + # 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 - headers = { - Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", + base_headers = { + Constants.Headers.AUTHORIZATION: effective_auth, "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/crewai/sample_agent/.env.template b/python/crewai/sample_agent/.env.template index a4faa8bb..91eb2c67 100644 --- a/python/crewai/sample_agent/.env.template +++ b/python/crewai/sample_agent/.env.template @@ -1,143 +1,191 @@ # ============================================================================= -# 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_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 UPN (User Principal Name) — identity of the agent in Microsoft 365 +# e.g. my-agent@contoso.onmicrosoft.com +AGENTIC_UPN=<> -# ----------------------------------------------------------------------------- -# 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= - -# ----------------------------------------------------------------------------- -# OBSERVABILITY CONFIGURATION -# ----------------------------------------------------------------------------- -# These settings configure Agent 365 observability tracing +# Display name of the agent as registered in Microsoft 365 +AGENTIC_NAME=<> -# Service identifiers for telemetry -OBSERVABILITY_SERVICE_NAME=crewai-agent-sample -OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples +# Object ID (user ID) of the agent service principal in Azure AD +AGENTIC_USER_ID=<> -# Enable/disable observability features -ENABLE_OBSERVABILITY=true +# Tenant ID where the agent is registered +AGENTIC_TENANT_ID=<> -# Enable Agent 365 cloud exporter (requires valid token) -# Set to "true" to send traces to Agent 365 observability backend -ENABLE_A365_OBSERVABILITY_EXPORTER=false - -# Python environment indicator -PYTHON_ENVIRONMENT=development - -# ----------------------------------------------------------------------------- -# SERVER CONFIGURATION -# ----------------------------------------------------------------------------- -# The port the agent server listens on -# If busy, the server will automatically try the next available port -PORT=3978 - -# Logging verbosity: DEBUG, INFO, WARNING, ERROR -LOG_LEVEL=INFO +# ============================================================================= +# AUTHENTICATION +# ============================================================================= -# ----------------------------------------------------------------------------- -# MCP SERVER CONFIGURATION (ADVANCED) -# ----------------------------------------------------------------------------- -# These are typically not needed for standard usage +# --- 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=<> -# Local MCP server settings (for custom MCP server development) -MCP_SERVER_PORT= -MCP_SERVER_HOST= -MCP_DEVELOPMENT_BASE_URL= +# --- Option B: Agentic Authentication (production / Teams deployment) --- +# Set USE_AGENTIC_AUTH=true and fill in the CONNECTIONS__ block below. +USE_AGENTIC_AUTH=false -# ----------------------------------------------------------------------------- -# AGENT 365 APP REGISTRATION (PRODUCTION) -# ----------------------------------------------------------------------------- -# Required for production deployments with full authentication -# Get these from Azure Portal > App Registrations > Your App +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. +AUTH_HANDLER_NAME= -# Azure AD App Registration -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 +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default -# Service URL mapping +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# 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=<> + +# 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 + +# 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= + +# --- 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 +# ============================================================================= + +# 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/crewai/sample_agent/ToolingManifest.json b/python/crewai/sample_agent/ToolingManifest.json index b8fe9815..bae107fb 100644 --- a/python/crewai/sample_agent/ToolingManifest.json +++ b/python/crewai/sample_agent/ToolingManifest.json @@ -1,39 +1,20 @@ { "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_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_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_MeServer", - "mcpServerUniqueName": "mcp_MeServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MeServer", - "scope": "McpServers.Me.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "scope": "Tools.ListInvoke.All", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", + "publisher": "Microsoft" }, { - "mcpServerName": "mcp_TeamsServer", - "mcpServerUniqueName": "mcp_TeamsServer", - "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer", - "scope": "McpServers.Teams.All", - "audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1" + "mcpServerName": "mcp_MailTools", + "mcpServerUniqueName": "mcp_MailTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools", + "scope": "Tools.ListInvoke.All", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} 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 ebdbefe5..2e368c75 100644 --- a/python/crewai/sample_agent/mcp_tool_registration_service.py +++ b/python/crewai/sample_agent/mcp_tool_registration_service.py @@ -21,11 +21,13 @@ import logging import os import random +import time import aiohttp import asyncio 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, @@ -97,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. @@ -126,12 +148,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}") @@ -216,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") @@ -234,39 +268,65 @@ async def discover_and_connect_servers( mcp_server_configs = [] try: self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") + + # 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, - auth_token=auth_token if auth_token else None, + auth_token=auth_token or "", + options=options, + **list_kwargs, ) - + # 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 +346,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 +385,49 @@ 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: + # 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 - headers = { - Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", + base_headers = { + Constants.Headers.AUTHORIZATION: effective_auth, "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, @@ -661,7 +735,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. @@ -674,9 +748,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 fe39cd21..2cf3256b 100644 --- a/python/google-adk/sample-agent/.env.template +++ b/python/google-adk/sample-agent/.env.template @@ -1,27 +1,83 @@ # ============================================================================= -# Google ADK Sample Agent — Environment Configuration +# Google ADK Agent Sample — Environment Configuration # ============================================================================= -# Copy this file to .env and fill in your values: -# cp .env.template .env -# -# All values marked <<...>> MUST be replaced before the agent will work. -# Run `a365 config init` first — it generates the config files referenced below. +# 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 Gemini Configuration +# 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=<> + +# Gemini model to use GEMINI_MODEL=gemini-2.5-flash + +# Vertex AI settings — required when GOOGLE_GENAI_USE_VERTEXAI=TRUE GOOGLE_CLOUD_PROJECT=<> GOOGLE_CLOUD_LOCATION=<> -GOOGLE_GENAI_USE_VERTEXAI=TRUE -# ----------------------------------------------------------------------------- -# Agent365 Service Connection (OAuth client credentials) -# ----------------------------------------------------------------------------- -# These values authenticate your agent with the Bot Framework and Agent 365. -# + +# ============================================================================= +# 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-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 +# This token expires — regenerate when you see 401 / MCP connection errors. +# 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. +# "" — local dev / Agents Playground. Allows anonymous access. +AUTH_HANDLER_NAME= + +# OAuth scope for the initial blueprint token exchange with the Agent 365 platform +AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default + +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when AUTH_HANDLER_NAME=AGENTIC) +# ============================================================================= # Where to find them (after running `a365 config init`): # CLIENTID => a365.generated.config.json → agentBlueprintId # CLIENTSECRET => a365.generated.config.json → agentBlueprintClientSecret @@ -34,67 +90,59 @@ GOOGLE_GENAI_USE_VERTEXAI=TRUE # 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=<> + +# 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. CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default -# Agentic user-authorization handler settings (do not change these defaults) +# 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 -# Connection map (do not change) +# 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 Identity -# ----------------------------------------------------------------------------- -# These identify your agent in the Agent 365 ecosystem. -# -# Where to find them: -# AGENTIC_UPN => a365.config.json → agentUserPrincipalName -# AGENTIC_NAME => a365.config.json → agentUserDisplayName -# AGENTIC_USER_ID => a365.generated.config.json → AgenticUserId -# AGENTIC_APP_ID => a365.generated.config.json → AgenticAppId -# AGENTIC_TENANT_ID => a365.config.json → tenantId -# -# NOTE: AGENTIC_APP_ID is the agentic app ID, which is different from the -# blueprint ID (CLIENTID above). Do not use AGENTIC_APP_ID for JWT validation. -AGENTIC_UPN=<> -AGENTIC_NAME=<> -AGENTIC_USER_ID=<> -AGENTIC_APP_ID=<> -AGENTIC_TENANT_ID=<> - -# ----------------------------------------------------------------------------- -# Local Development -# ----------------------------------------------------------------------------- -# Bearer token for local dev / Playground — obtain with: a365 develop get-token -o raw -# Leave empty to run in bare LLM mode (no MCP tools) -BEARER_TOKEN= - -# Authentication handler: -# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. -# "" — local dev / Agents Playground. Allows anonymous access. -AUTH_HANDLER_NAME= +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= -# ----------------------------------------------------------------------------- -# Server -# ----------------------------------------------------------------------------- -# Port for the aiohttp server. -# Local dev default: 3978. Azure App Service injects PORT automatically. +# Port the HTTP server listens on (default: 3978 — must match your bot channel endpoint) PORT=3978 -# Logging level: DEBUG, INFO, WARNING, ERROR +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL LOG_LEVEL=INFO -# ----------------------------------------------------------------------------- -# Observability -# ----------------------------------------------------------------------------- +# ============================================================================= +# 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 -OBSERVABILITY_SERVICE_NAME=GoogleADKSampleAgent -OBSERVABILITY_SERVICE_NAMESPACE=GoogleADKTesting diff --git a/python/google-adk/sample-agent/ToolingManifest.json b/python/google-adk/sample-agent/ToolingManifest.json index e842561c..496a3a58 100644 --- a/python/google-adk/sample-agent/ToolingManifest.json +++ b/python/google-adk/sample-agent/ToolingManifest.json @@ -1,11 +1,20 @@ { "mcpServers": [ + { + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", + "scope": "Tools.ListInvoke.All", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", + "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" + "scope": "Tools.ListInvoke.All", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", + "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..4cfd3e30 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) @@ -185,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 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_") and k != "BEARER_TOKEN"]: + 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/main.py b/python/google-adk/sample-agent/main.py index d3378700..7ac7f853 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 @@ -33,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: @@ -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,7 +135,7 @@ async def health_check(req: Request) -> Response: else: port = 3978 logger.info("PORT not set, using default: %d", port) - + logger.info("Listening on %s:%d/api/messages", host, port) run_app(app, host=host, port=port, handle_signals=True) except KeyboardInterrupt: 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 9e50ea83..b32844e0 100644 --- a/python/google-adk/sample-agent/mcp_tool_registration_service.py +++ b/python/google-adk/sample-agent/mcp_tool_registration_service.py @@ -9,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. @@ -48,31 +53,47 @@ 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() auth_token_obj = await auth.exchange_token(context, scopes, auth_handler_name) 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 - ) + agentic_app_id=agentic_app_id, + auth_token=auth_token, + options=options, + **list_kwargs, + ) self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations") - # Convert MCP server configs to MCPServerInfo objects + # Convert MCP server configs to McpToolset objects mcp_servers_info = [] - mcp_server_headers = { - "Authorization": f"Bearer {auth_token}" - } for server_config in mcp_server_configs: if not server_config.url: @@ -81,10 +102,31 @@ async def add_tool_servers_to_agent( server_config.mcp_server_unique_name, ) continue + + # server_config.headers already contains the per-audience Authorization 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( + self._orchestrator_name + ) + } + 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, - headers=mcp_server_headers + 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", ] diff --git a/python/openai/sample-agent/.env.template b/python/openai/sample-agent/.env.template index c095efb8..a4072efe 100644 --- a/python/openai/sample-agent/.env.template +++ b/python/openai/sample-agent/.env.template @@ -1,47 +1,164 @@ -# This is a demo .env file -# Replace with your actual OpenAI API key -OPENAI_API_KEY= +# ============================================================================= +# 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. +# ============================================================================= -# MCP Server Configuration +# ----------------------------------------------------------------------------- +# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one) +# ----------------------------------------------------------------------------- + +# --- Option A: Standard OpenAI --- +# Get your API key from https://platform.openai.com/api-keys +OPENAI_API_KEY=<> + +# OpenAI model to use (e.g. gpt-4o, gpt-4o-mini) +OPENAI_MODEL=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 + +# ============================================================================= +# 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-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 +# ============================================================================= + +# --- 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=false + +# Authentication handler: +# "AGENTIC" — production (Teams / Azure deployment). Enforces agentic auth on message handlers. +# "" — local dev / Agents Playground. Allows anonymous access. +AUTH_HANDLER_NAME= + +# 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 +# ============================================================================= + +# Override the default MCP platform base URL +MCP_DEVELOPMENT_BASE_URL= + +# 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 -MCP_DEVELOPMENT_BASE_URL= -# Logging -LOG_LEVEL=INFO +# --- 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 -# Observability Configuration -OBSERVABILITY_SERVICE_NAME=openai-agent-sample -OBSERVABILITY_SERVICE_NAMESPACE=agents.samples +# ============================================================================= +# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true) +# ============================================================================= +# 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. -BEARER_TOKEN= -OPENAI_MODEL=gpt-4o-mini +# Azure AD Application (client) ID of your bot/agent app registration +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<> -USE_AGENTIC_AUTH= +# Client secret created in Azure Portal > your app > Certificates & secrets > New client secret +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<> -AGENT_ID= +# Azure AD Tenant (directory) ID where your app is registered +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<> -# Agent 365 Agentic Authentication Configuration -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 + +# 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=AGENTBLUEPRINT +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="gpt-4o-mini" +# Log verbosity: DEBUG | INFO | WARNING | ERROR | CRITICAL +LOG_LEVEL=INFO + +# ============================================================================= +# OBSERVABILITY (Agent 365 Telemetry) +# ============================================================================= -# Required for observability SDK +# Master switch — enable OpenTelemetry tracing for this agent ENABLE_OBSERVABILITY=true -ENABLE_KAIRO_EXPORTER=true -PYTHON_ENVIRONMENT=production + +# 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 diff --git a/python/openai/sample-agent/ToolingManifest.json b/python/openai/sample-agent/ToolingManifest.json index e842561c..bae107fb 100644 --- a/python/openai/sample-agent/ToolingManifest.json +++ b/python/openai/sample-agent/ToolingManifest.json @@ -1,11 +1,20 @@ { "mcpServers": [ + { + "mcpServerName": "mcp_CalendarTools", + "mcpServerUniqueName": "mcp_CalendarTools", + "url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools", + "scope": "Tools.ListInvoke.All", + "audience": "910333d2-47e9-43ca-981f-6df2f4531ef4", + "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" + "scope": "Tools.ListInvoke.All", + "audience": "16b1878d-62c7-4009-aa25-68989d63bbad", + "publisher": "Microsoft" } ] -} \ No newline at end of file +} 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: