Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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 @@ -106,3 +106,12 @@ coverage/
# OS-specific files
.DS_Store
Thumbs.db

# Agent 365 generated config and deployment artifacts
a365.config.json
a365.generated.config.json
app.zip
app_logs.zip
app_logs/
publish/
manifest/
133 changes: 105 additions & 28 deletions python/agent-framework/sample-agent/.env.template
Original file line number Diff line number Diff line change
@@ -1,52 +1,129 @@
# This is a demo .env file
# Replace with your actual OpenAI API key
OPENAI_API_KEY=
MCP_SERVER_HOST=
MCP_PLATFORM_ENDPOINT=
# =============================================================================
# Agent Framework Agent Sample — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in ALL required values before running.
# Lines starting with # are comments. Never commit .env to source control.
# =============================================================================

# Authentication Handler Configuration
# Set to "AGENTIC" for production agentic auth, or leave empty for no auth handler
AUTH_HANDLER_NAME=
# -----------------------------------------------------------------------------
# OPENAI / AZURE OPENAI CONFIGURATION (REQUIRED — choose one)
# -----------------------------------------------------------------------------

# Logging
LOG_LEVEL=INFO
# --- Option A: Standard OpenAI ---
# Get your API key from https://platform.openai.com/api-keys
OPENAI_API_KEY=<<YOUR_OPENAI_API_KEY>>

# Observability Configuration
OBSERVABILITY_SERVICE_NAME=agent-framework-sample
OBSERVABILITY_SERVICE_NAMESPACE=agent-framework.samples
# 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 Agent 365 portal.
# Find this in: Azure Portal > App Registrations > your agent app > Application (client) ID
AGENTIC_APP_ID=<<YOUR_AGENT_APP_ID_GUID>>

BEARER_TOKEN=
OPENAI_MODEL=
# 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>>

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

# Agent 365 Agentic Authentication Configuration
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=
# Name of the auth handler configured in your app registration (default: AGENTIC)
AUTH_HANDLER_NAME=AGENTIC

# =============================================================================
# MCP (MODEL CONTEXT PROTOCOL) — ADVANCED
# =============================================================================

# Hostname for a locally running custom MCP server (leave blank if not using one)
MCP_SERVER_HOST=

# =============================================================================
# AGENT365 SERVICE CONNECTION (Required when USE_AGENTIC_AUTH=true)
# =============================================================================
# These map to appsettings-style connection configuration loaded by the SDK.
# Get these values from: Azure Portal > App Registrations > your app > Certificates & secrets

# Azure AD Application (client) ID of your bot/agent app registration
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<<AZURE_AD_CLIENT_ID>>

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

# Required for observability SDK
# Logical service name shown in traces / dashboards
OBSERVABILITY_SERVICE_NAME=agent-framework-sample

# Namespace grouping for this sample in telemetry backends
OBSERVABILITY_SERVICE_NAMESPACE=agent-framework.samples

# Master switch — enable OpenTelemetry tracing for this agent
ENABLE_OBSERVABILITY=true

# Set to "true" to ship traces to the Agent 365 cloud observability backend.
# Requires a valid token and is intended for production / staging deployments.
ENABLE_A365_OBSERVABILITY_EXPORTER=false

# Python environment label — influences routing and cluster selection in Agent 365
# Options: development | production
PYTHON_ENVIRONMENT=development

# Enable otel logs on AgentFramework SDK. Required for auto instrumentation
# Enable OpenTelemetry logs on the Agent Framework SDK (required for auto-instrumentation)
ENABLE_OTEL=true

# Set to "true" to include request/response payloads in traces (CAUTION: may log PII)
ENABLE_SENSITIVE_DATA=true
34 changes: 31 additions & 3 deletions python/agent-framework/sample-agent/ToolingManifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
{
"mcpServers": [
{
"mcpServerName": "mcp_MailTools",
"mcpServerUniqueName": "mcp_MailTools"
"mcpServerName": "mcp_Admin365_GraphTools",
"mcpServerUniqueName": "mcp_Admin365_GraphTools",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_Admin365_GraphTools",
"scope": "McpServers.Admin365Graph.All",
"audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_OneDriveRemoteServer",
"mcpServerUniqueName": "mcp_OneDriveRemoteServer",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_OneDriveRemoteServer",
"scope": "Tools.ListInvoke.All",
"audience": "26807933-9b72-4e7a-bd73-f6c86ba42e73",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_SharePointRemoteServer",
"mcpServerUniqueName": "mcp_SharePointRemoteServer",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_SharePointRemoteServer",
"scope": "Tools.ListInvoke.All",
"audience": "b154d24d-a357-4961-ba54-65e171c9cb05",
"publisher": "Microsoft"
},
{
"mcpServerName": "mcp_TeamsServerV1",
"mcpServerUniqueName": "mcp_TeamsServerV1",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServerV1",
"scope": "McpServers.Teams.All",
"audience": "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1",
"publisher": "Microsoft"
}
]
}
}
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
2 changes: 2 additions & 0 deletions python/agent-framework/sample-agent/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
77 changes: 77 additions & 0 deletions python/agent-framework/sample-agent/tests/test_tooling_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""
Tests for agent-framework ToolingManifest.json structure.
Validates V2 MCP fields are present and correctly formed.
"""

import json
import os
import pytest

MANIFEST_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"ToolingManifest.json",
)

MCP_SERVERS_ALL_PATTERN = "McpServers."
V2_AUDIENCE = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1"


@pytest.fixture(scope="module")
def manifest():
with open(MANIFEST_PATH) as f:
return json.load(f)


@pytest.fixture(scope="module")
def servers(manifest):
return manifest["mcpServers"]


class TestManifestStructure:
def test_manifest_has_mcp_servers_key(self, manifest):
assert "mcpServers" in manifest

def test_at_least_one_server(self, servers):
assert len(servers) > 0

def test_each_server_has_required_fields(self, servers):
required = {"mcpServerName", "mcpServerUniqueName", "url", "scope", "audience", "publisher"}
for s in servers:
missing = required - s.keys()
assert not missing, f"Server '{s.get('mcpServerName')}' missing fields: {missing}"

def test_urls_are_https(self, servers):
for s in servers:
assert s["url"].startswith("https://"), f"Server '{s['mcpServerName']}' URL must be HTTPS"

def test_urls_point_to_production_endpoint(self, servers):
for s in servers:
assert "agent365.svc.cloud.microsoft" in s["url"], (
f"Server '{s['mcpServerName']}' should use production endpoint"
)

def test_no_null_scopes(self, servers):
for s in servers:
assert s["scope"] and s["scope"] != "null", (
f"Server '{s['mcpServerName']}' has null/empty scope"
)

def test_mcp_servers_all_scopes_use_v2_audience(self, servers):
"""Servers with McpServers.*.All scope must use the V2 audience GUID."""
for s in servers:
if s["scope"].startswith(MCP_SERVERS_ALL_PATTERN):
assert s["audience"] == V2_AUDIENCE, (
f"Server '{s['mcpServerName']}' with scope '{s['scope']}' "
f"must use audience '{V2_AUDIENCE}'"
)

def test_publisher_is_set(self, servers):
for s in servers:
assert s["publisher"], f"Server '{s['mcpServerName']}' has empty publisher"

def test_no_duplicate_server_names(self, servers):
names = [s["mcpServerName"] for s in servers]
assert len(names) == len(set(names)), "Duplicate mcpServerName entries found"
Loading
Loading