Skip to content

Commit 6eec09a

Browse files
authored
OTel enhancements for deployed server (Azure-Samples#21)
* Add Entra auth proxy * Reverting unneeded changes * Fixes to env vars and readme * Fix ManagedIdentityCredential in prod so it knows to use user-assigned * Address PR feedback * Move to a single variable for auth control * Fix ruff error * Revert mcp.json changes * Fix ps1 auth init * Tweaks while working on slides * Updates to OTel and README * Remove restore * Readme fix * Log level fix * Remove keycloak from agentframework example
1 parent 867161e commit 6eec09a

13 files changed

+209
-100
lines changed

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ The `.vscode/launch.json` provides a debug configuration to attach to an MCP ser
121121
3. Press `Cmd+Shift+D` to open Run and Debug
122122
4. Select "Attach to MCP Server (stdio)" configuration
123123
5. Press `F5` or the play button to start the debugger
124-
6. Select the expenses-mcp-debug server in GitHub Copilot Chat tools
124+
6. Select the "expenses-mcp-debug" server in GitHub Copilot Chat tools
125125
7. Use GitHub Copilot Chat to trigger the MCP tools
126126
8. Debugger pauses at breakpoints
127127

@@ -191,7 +191,6 @@ You can use the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspir
191191
uv run servers/basic_mcp_http.py
192192
```
193193

194-
195194
4. View the dashboard at: http://localhost:18888
196195

197196
---
@@ -288,6 +287,58 @@ You can try the [Azure pricing calculator](https://azure.com/e/3987c81282c84410b
288287

289288
⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use, either by deleting the resource group in the Portal or running `azd down`.
290289

290+
### Use deployed MCP server with GitHub Copilot
291+
292+
The URL of the deployed MCP server is available in the azd environment variable `MCP_SERVER_URL`, and is written to the `.env` file created after deployment.
293+
294+
1. To avoid conflicts, stop the MCP servers from `mcp.json` and disable the expense MCP servers in GitHub Copilot Chat tools.
295+
2. Select "MCP: Add Server" from the VS Code Command Palette
296+
3. Select "HTTP" as the server type
297+
4. Enter the URL of the MCP server, based on the `MCP_SERVER_URL` environment variable.
298+
5. Enable the MCP server in GitHub Copilot Chat tools and test it with an expense tracking query:
299+
300+
```text
301+
Log expense for 75 dollars of office supplies on my visa last Friday
302+
```
303+
304+
### Running the server locally
305+
306+
After deployment sets up the required Azure resources (Cosmos DB, Application Insights), you can also run the MCP server locally against those resources:
307+
308+
```bash
309+
# Run the MCP server
310+
cd servers && uvicorn deployed_mcp:app --host 0.0.0.0 --port 8000
311+
```
312+
313+
### Viewing traces in Azure Application Insights
314+
315+
By default, OpenTelemetry tracing is enabled for the deployed MCP server, sending traces to Azure Application Insights.
316+
317+
1. Open the Azure Portal and navigate to the Application Insights resource created during deployment (named `<project-name>-appinsights`).
318+
2. In Application Insights, go to "Transaction Search" to view traces from the MCP server
319+
3. You can filter and analyze traces to monitor performance and diagnose issues.
320+
321+
### Viewing traces in Logfire
322+
323+
You can also view OpenTelemetry traces in [Logfire](https://logfire.io/) by configuring the MCP server to send traces there.
324+
325+
1. Create a Logfire account and get your write token from the Logfire dashboard.
326+
327+
2. Set the azd environment variables to enable Logfire:
328+
329+
```bash
330+
azd env set OPENTELEMETRY_PLATFORM logfire
331+
azd env set LOGFIRE_TOKEN <your-logfire-write-token>
332+
```
333+
334+
3. Provision and deploy:
335+
336+
```bash
337+
azd up
338+
```
339+
340+
4. Open the Logfire dashboard to view traces from the MCP server.
341+
291342
---
292343

293344
## Deploy to Azure with private networking
@@ -461,7 +512,7 @@ The following environment variables are automatically set by the deployment hook
461512

462513
These are then written to `.env` by the postprovision hook for local development.
463514

464-
### Testing locally
515+
### Testing the Entra OAuth server locally
465516

466517
After deployment, you can test locally with OAuth enabled:
467518

agents/agentframework_http.py

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
import asyncio
42
import logging
53
import os
@@ -13,24 +11,18 @@
1311
from rich import print
1412
from rich.logging import RichHandler
1513

16-
try:
17-
from keycloak_auth import get_auth_headers
18-
except ImportError:
19-
from agents.keycloak_auth import get_auth_headers
20-
2114
# Configure logging
2215
logging.basicConfig(level=logging.WARNING, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()])
2316
logger = logging.getLogger("agentframework_mcp_http")
17+
logger.setLevel(logging.INFO)
2418

25-
# Load environment variables
26-
load_dotenv(override=True)
27-
28-
# Constants
19+
# Configure constants and client based on environment
2920
RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true"
30-
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")
3121

32-
# Optional: Keycloak authentication (set KEYCLOAK_REALM_URL to enable)
33-
KEYCLOAK_REALM_URL = os.getenv("KEYCLOAK_REALM_URL")
22+
if not RUNNING_IN_PRODUCTION:
23+
load_dotenv(override=True)
24+
25+
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")
3426

3527
# Configure chat client based on API_HOST
3628
API_HOST = os.getenv("API_HOST", "github")
@@ -61,24 +53,9 @@
6153

6254

6355
# --- Main Agent Logic ---
64-
65-
6656
async def http_mcp_example() -> None:
67-
"""
68-
Demonstrate MCP integration with the Expenses MCP server.
69-
70-
If KEYCLOAK_REALM_URL is set, authenticates via OAuth (DCR + client credentials).
71-
Otherwise, connects without authentication.
72-
"""
73-
# Get auth headers if Keycloak is configured
74-
headers = await get_auth_headers(KEYCLOAK_REALM_URL, client_name_prefix="agentframework")
75-
if headers:
76-
logger.info(f"🔐 Auth enabled - connecting to {MCP_SERVER_URL} with Bearer token")
77-
else:
78-
logger.info(f"📡 No auth - connecting to {MCP_SERVER_URL}")
79-
8057
async with (
81-
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL, headers=headers) as mcp_server,
58+
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL) as mcp_server,
8259
ChatAgent(
8360
chat_client=client,
8461
name="Expenses Agent",

agents/agentframework_learn.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,7 @@ async def http_mcp_example() -> None:
5858
using the Microsoft Learn MCP server.
5959
"""
6060
async with (
61-
MCPStreamableHTTPTool(
62-
name="Microsoft Learn MCP",
63-
url=LEARN_MCP_URL,
64-
headers={"Authorization": "Bearer your-token"},
65-
) as mcp_server,
61+
MCPStreamableHTTPTool(name="Microsoft Learn MCP", url=LEARN_MCP_URL) as mcp_server,
6662
ChatAgent(
6763
chat_client=client,
6864
name="DocsAgent",

agents/langchainv1_github.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async def main():
9090
prompt="You help users research GitHub repositories. Search and analyze information.",
9191
)
9292

93-
query = "Find popular Python MCP server repositories"
93+
query = "Find 5 popular Python MCP server repositories and describe in a bulleted list."
9494
rprint(f"[bold]Query:[/bold] {query}\n")
9595

9696
try:

infra/main.bicep

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,16 @@ param keycloakExists bool = false
3737
// This does not need a default value, as azd will prompt the user to select a location
3838
param openAiResourceLocation string
3939

40-
@description('Flag to enable or disable monitoring resources')
41-
param useMonitoring bool = true
40+
@description('OpenTelemetry platform for monitoring: appinsights, logfire, or none')
41+
@allowed([
42+
'appinsights'
43+
'logfire'
44+
'none'
45+
])
46+
param openTelemetryPlatform string = 'appinsights'
47+
48+
// Derived boolean for App Insights resource creation
49+
var useAppInsights = openTelemetryPlatform == 'appinsights'
4250

4351
@description('Flag to enable or disable the virtual network feature')
4452
param useVnet bool = false
@@ -80,6 +88,10 @@ param entraProxyClientId string = ''
8088
@description('Azure/Entra ID app registration client secret for OAuth Proxy - required when mcpAuthProvider is entra_proxy')
8189
param entraProxyClientSecret string = ''
8290

91+
@secure()
92+
@description('Logfire token used by the server container as a secret')
93+
param logfireToken string = ''
94+
8395
// Derived booleans for backward compatibility in bicep modules
8496
var useKeycloak = mcpAuthProvider == 'keycloak'
8597
var useEntraProxy = mcpAuthProvider == 'entra_proxy'
@@ -119,7 +131,7 @@ module openAi 'br/public:avm/res/cognitive-services/account:0.7.2' = {
119131
bypass: 'AzureServices'
120132
}
121133
sku: 'S0'
122-
diagnosticSettings: useMonitoring
134+
diagnosticSettings: useAppInsights
123135
? [
124136
{
125137
name: 'customSetting'
@@ -196,7 +208,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.6.1' = {
196208
}
197209
}
198210

199-
module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = if (useMonitoring) {
211+
module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = if (useAppInsights) {
200212
name: 'loganalytics'
201213
scope: resourceGroup
202214
params: {
@@ -212,7 +224,7 @@ module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0
212224
}
213225

214226
// Application Insights for telemetry
215-
module applicationInsights 'br/public:avm/res/insights/component:0.4.2' = if (useMonitoring) {
227+
module applicationInsights 'br/public:avm/res/insights/component:0.4.2' = if (useAppInsights) {
216228
name: 'applicationinsights'
217229
scope: resourceGroup
218230
params: {
@@ -411,7 +423,7 @@ module openAiPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' =
411423
}
412424

413425
// Log Analytics Private DNS Zone
414-
module logAnalyticsPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useMonitoring) {
426+
module logAnalyticsPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useAppInsights) {
415427
name: 'log-analytics-dns-zone'
416428
scope: resourceGroup
417429
params: {
@@ -427,7 +439,7 @@ module logAnalyticsPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.
427439
}
428440

429441
// Additional Log Analytics Private DNS Zone for query endpoint
430-
module logAnalyticsQueryPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useMonitoring) {
442+
module logAnalyticsQueryPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useAppInsights) {
431443
name: 'log-analytics-query-dns-zone'
432444
scope: resourceGroup
433445
params: {
@@ -443,7 +455,7 @@ module logAnalyticsQueryPrivateDnsZone 'br/public:avm/res/network/private-dns-zo
443455
}
444456

445457
// Additional Log Analytics Private DNS Zone for agent service
446-
module logAnalyticsAgentPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useMonitoring) {
458+
module logAnalyticsAgentPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useAppInsights) {
447459
name: 'log-analytics-agent-dns-zone'
448460
scope: resourceGroup
449461
params: {
@@ -459,7 +471,7 @@ module logAnalyticsAgentPrivateDnsZone 'br/public:avm/res/network/private-dns-zo
459471
}
460472

461473
// Azure Monitor Private DNS Zone
462-
module monitorPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useMonitoring) {
474+
module monitorPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useAppInsights) {
463475
name: 'monitor-dns-zone'
464476
scope: resourceGroup
465477
params: {
@@ -475,7 +487,7 @@ module monitorPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1'
475487
}
476488

477489
// Storage Blob Private DNS Zone for Log Analytics solution packs
478-
module blobPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useMonitoring) {
490+
module blobPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (useVnet && useAppInsights) {
479491
name: 'blob-dns-zone'
480492
scope: resourceGroup
481493
params: {
@@ -599,7 +611,7 @@ module privateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.0' = if
599611
}
600612

601613
// Azure Monitor Private Link Scope
602-
module monitorPrivateLinkScope 'br/public:avm/res/insights/private-link-scope:0.7.1' = if (useVnet && useMonitoring) {
614+
module monitorPrivateLinkScope 'br/public:avm/res/insights/private-link-scope:0.7.1' = if (useVnet && useAppInsights) {
603615
name: 'monitor-private-link-scope'
604616
scope: resourceGroup
605617
params: {
@@ -654,7 +666,7 @@ module containerApps 'core/host/container-apps.bicep' = {
654666
tags: tags
655667
containerAppsEnvironmentName: '${prefix}-containerapps-env'
656668
containerRegistryName: '${take(replace(prefix, '-', ''), 42)}registry'
657-
logAnalyticsWorkspaceName: useMonitoring ? logAnalyticsWorkspace!.outputs.name : ''
669+
logAnalyticsWorkspaceName: useAppInsights ? logAnalyticsWorkspace!.outputs.name : ''
658670
// Reference the virtual network only if useVnet is true
659671
subnetResourceId: useVnet ? virtualNetwork!.outputs.subnetResourceIds[0] : ''
660672
vnetName: useVnet ? virtualNetwork!.outputs.name : ''
@@ -746,7 +758,8 @@ module server 'server.bicep' = {
746758
cosmosDbContainer: cosmosDbContainerName
747759
cosmosDbUserContainer: cosmosDbUserContainerName
748760
cosmosDbOAuthContainer: cosmosDbOAuthContainerName
749-
applicationInsightsConnectionString: useMonitoring ? applicationInsights!.outputs.connectionString : ''
761+
applicationInsightsConnectionString: useAppInsights ? applicationInsights!.outputs.connectionString : ''
762+
openTelemetryPlatform: openTelemetryPlatform
750763
exists: serverExists
751764
// Keycloak authentication configuration (only when enabled)
752765
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/realms/${keycloakRealmName}' : ''
@@ -759,6 +772,7 @@ module server 'server.bicep' = {
759772
entraProxyBaseUrl: useEntraProxy ? entraProxyMcpServerBaseUrl : ''
760773
tenantId: useEntraProxy ? tenant().tenantId : ''
761774
mcpAuthProvider: mcpAuthProvider
775+
logfireToken: logfireToken
762776
}
763777
}
764778

@@ -897,7 +911,7 @@ output AZURE_COSMOSDB_USER_CONTAINER string = cosmosDbUserContainerName
897911
output AZURE_COSMOSDB_OAUTH_CONTAINER string = cosmosDbOAuthContainerName
898912

899913
// We typically do not output sensitive values, but App Insights connection strings are not considered highly sensitive
900-
output APPLICATIONINSIGHTS_CONNECTION_STRING string = useMonitoring ? applicationInsights!.outputs.connectionString : ''
914+
output APPLICATIONINSIGHTS_CONNECTION_STRING string = useAppInsights ? applicationInsights!.outputs.connectionString : ''
901915

902916
// Entry selection for MCP server (auth-enabled when Keycloak or FastMCP auth is used)
903917
// Use server module's computed entry selection (checks URLs/clientId)
@@ -918,3 +932,6 @@ output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}
918932

919933
// Auth provider for env scripts
920934
output MCP_AUTH_PROVIDER string = mcpAuthProvider
935+
936+
// OpenTelemetry platform for env scripts
937+
output OPENTELEMETRY_PLATFORM string = openTelemetryPlatform

infra/main.parameters.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
"principalId": {
1212
"value": "${AZURE_PRINCIPAL_ID}"
1313
},
14-
"useMonitoring": {
15-
"value": "${USE_MONITORING=true}"
14+
"openTelemetryPlatform": {
15+
"value": "${OPENTELEMETRY_PLATFORM=appinsights}"
1616
},
1717
"useVnet": {
1818
"value": "${USE_VNET=false}"
@@ -50,12 +50,14 @@
5050
"keycloakMcpServerAudience": {
5151
"value": "${KEYCLOAK_MCP_SERVER_AUDIENCE=mcp-server}"
5252
},
53-
5453
"entraProxyClientId": {
5554
"value": "${ENTRA_PROXY_AZURE_CLIENT_ID}"
5655
},
5756
"entraProxyClientSecret": {
5857
"value": "${ENTRA_PROXY_AZURE_CLIENT_SECRET}"
58+
},
59+
"logfireToken": {
60+
"value": "${LOGFIRE_TOKEN}"
5961
}
6062
}
6163
}

0 commit comments

Comments
 (0)