Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
169 changes: 141 additions & 28 deletions python/agent-framework/sample-agent/.env.template
Original file line number Diff line number Diff line change
@@ -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=<<YOUR_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_API_KEY>>
AZURE_OPENAI_ENDPOINT=<<AZURE_OPENAI_ENDPOINT_URL>> # e.g. https://my-resource.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=<<DEPLOYMENT_NAME>> # 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=<<YOUR_AGENT_APP_ID_GUID>>

# 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=<<YOUR_AGENT_ID>>

# Agent UPN (User Principal Name) — identity of the agent in Microsoft 365
# e.g. my-agent@contoso.onmicrosoft.com
AGENTIC_UPN=<<YOUR_AGENT_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=<<YOUR_AGENT_NAME>>

# Object ID (user ID) of the agent service principal in Azure AD
AGENTIC_USER_ID=<<YOUR_AGENTIC_USER_ID>>

# Tenant ID where the agent is registered
AGENTIC_TENANT_ID=<<YOUR_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=<<PASTE_DEV_TOKEN_HERE_OR_LEAVE_BLANK>>

# --- 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_<UPPERCASE_SERVER_NAME> 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=<<TOKEN_FOR_MAIL_TOOLS>>
# 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=<<AZURE_AD_CLIENT_ID>>

# 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_CLIENT_SECRET>>

# Azure AD Tenant (directory) ID where your app is registered
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<<AZURE_AD_TENANT_ID>>

# 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
15 changes: 12 additions & 3 deletions python/agent-framework/sample-agent/ToolingManifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
36 changes: 35 additions & 1 deletion python/agent-framework/sample-agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import asyncio
import logging
import os
import time
from typing import Optional

from dotenv import load_dotenv
Expand Down Expand Up @@ -196,6 +197,27 @@ def _enable_agentframework_instrumentation(self):
# =========================================================================
# <McpServerSetup>

@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:
Expand All @@ -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,
Expand All @@ -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,
)

Expand Down
3 changes: 3 additions & 0 deletions python/agent-framework/sample-agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ dev-dependencies = [
"mypy>=1.0.0",
]

[tool.pytest.ini_options]
testpaths = ["tests"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down
Loading
Loading