Skip to content
Merged
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
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The `.vscode/launch.json` provides a debug configuration to attach to an MCP ser
3. Press `Cmd+Shift+D` to open Run and Debug
4. Select "Attach to MCP Server (stdio)" configuration
5. Press `F5` or the play button to start the debugger
6. Select the expenses-mcp-debug server in GitHub Copilot Chat tools
6. Select the "expenses-mcp-debug" server in GitHub Copilot Chat tools
7. Use GitHub Copilot Chat to trigger the MCP tools
8. Debugger pauses at breakpoints

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


4. View the dashboard at: http://localhost:18888

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

⚠️ 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`.

### Use deployed MCP server with GitHub Copilot

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.

1. To avoid conflicts, stop the MCP servers from `mcp.json` and disable the expense MCP servers in GitHub Copilot Chat tools.
2. Select "MCP: Add Server" from the VS Code Command Palette
3. Select "HTTP" as the server type
4. Enter the URL of the MCP server, based on the `MCP_SERVER_URL` environment variable.
5. Enable the MCP server in GitHub Copilot Chat tools and test it with an expense tracking query:

```text
Log expense for 75 dollars of office supplies on my visa last Friday
```

### Running the server locally

After deployment sets up the required Azure resources (Cosmos DB, Application Insights), you can also run the MCP server locally against those resources:

```bash
# Run the MCP server
cd servers && uvicorn deployed_mcp:app --host 0.0.0.0 --port 8000
```

### Viewing traces in Azure Application Insights

By default, OpenTelemetry tracing is enabled for the deployed MCP server, sending traces to Azure Application Insights.

1. Open the Azure Portal and navigate to the Application Insights resource created during deployment (named `<project-name>-appinsights`).
2. In Application Insights, go to "Transaction Search" to view traces from the MCP server
3. You can filter and analyze traces to monitor performance and diagnose issues.

### Viewing traces in Logfire

You can also view OpenTelemetry traces in [Logfire](https://logfire.io/) by configuring the MCP server to send traces there.

1. Create a Logfire account and get your write token from the Logfire dashboard.

2. Set the azd environment variables to enable Logfire:

```bash
azd env set OPENTELEMETRY_PLATFORM logfire
azd env set LOGFIRE_TOKEN <your-logfire-write-token>
```

3. Provision and deploy:

```bash
azd up
```

4. Open the Logfire dashboard to view traces from the MCP server.

---

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

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

### Testing locally
### Testing the Entra OAuth server locally

After deployment, you can test locally with OAuth enabled:

Expand Down
37 changes: 7 additions & 30 deletions agents/agentframework_http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import asyncio
import logging
import os
Expand All @@ -13,24 +11,18 @@
from rich import print
from rich.logging import RichHandler

try:
from keycloak_auth import get_auth_headers
except ImportError:
from agents.keycloak_auth import get_auth_headers

# Configure logging
logging.basicConfig(level=logging.WARNING, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()])
logger = logging.getLogger("agentframework_mcp_http")
logger.setLevel(logging.INFO)

# Load environment variables
load_dotenv(override=True)

# Constants
# Configure constants and client based on environment
RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true"
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")

# Optional: Keycloak authentication (set KEYCLOAK_REALM_URL to enable)
KEYCLOAK_REALM_URL = os.getenv("KEYCLOAK_REALM_URL")
if not RUNNING_IN_PRODUCTION:
load_dotenv(override=True)

MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")

# Configure chat client based on API_HOST
API_HOST = os.getenv("API_HOST", "github")
Expand Down Expand Up @@ -61,24 +53,9 @@


# --- Main Agent Logic ---


async def http_mcp_example() -> None:
"""
Demonstrate MCP integration with the Expenses MCP server.

If KEYCLOAK_REALM_URL is set, authenticates via OAuth (DCR + client credentials).
Otherwise, connects without authentication.
"""
# Get auth headers if Keycloak is configured
headers = await get_auth_headers(KEYCLOAK_REALM_URL, client_name_prefix="agentframework")
if headers:
logger.info(f"🔐 Auth enabled - connecting to {MCP_SERVER_URL} with Bearer token")
else:
logger.info(f"📡 No auth - connecting to {MCP_SERVER_URL}")

async with (
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL, headers=headers) as mcp_server,
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL) as mcp_server,
ChatAgent(
chat_client=client,
name="Expenses Agent",
Expand Down
6 changes: 1 addition & 5 deletions agents/agentframework_learn.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ async def http_mcp_example() -> None:
using the Microsoft Learn MCP server.
"""
async with (
MCPStreamableHTTPTool(
name="Microsoft Learn MCP",
url=LEARN_MCP_URL,
headers={"Authorization": "Bearer your-token"},
) as mcp_server,
MCPStreamableHTTPTool(name="Microsoft Learn MCP", url=LEARN_MCP_URL) as mcp_server,
ChatAgent(
chat_client=client,
name="DocsAgent",
Expand Down
2 changes: 1 addition & 1 deletion agents/langchainv1_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async def main():
prompt="You help users research GitHub repositories. Search and analyze information.",
)

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

try:
Expand Down
45 changes: 31 additions & 14 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@ param keycloakExists bool = false
// This does not need a default value, as azd will prompt the user to select a location
param openAiResourceLocation string

@description('Flag to enable or disable monitoring resources')
param useMonitoring bool = true
@description('OpenTelemetry platform for monitoring: appinsights, logfire, or none')
@allowed([
'appinsights'
'logfire'
'none'
])
param openTelemetryPlatform string = 'appinsights'

// Derived boolean for App Insights resource creation
var useAppInsights = openTelemetryPlatform == 'appinsights'

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

@secure()
@description('Logfire token used by the server container as a secret')
param logfireToken string = ''

// Derived booleans for backward compatibility in bicep modules
var useKeycloak = mcpAuthProvider == 'keycloak'
var useEntraProxy = mcpAuthProvider == 'entra_proxy'
Expand Down Expand Up @@ -119,7 +131,7 @@ module openAi 'br/public:avm/res/cognitive-services/account:0.7.2' = {
bypass: 'AzureServices'
}
sku: 'S0'
diagnosticSettings: useMonitoring
diagnosticSettings: useAppInsights
? [
{
name: 'customSetting'
Expand Down Expand Up @@ -196,7 +208,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.6.1' = {
}
}

module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = if (useMonitoring) {
module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = if (useAppInsights) {
name: 'loganalytics'
scope: resourceGroup
params: {
Expand All @@ -212,7 +224,7 @@ module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0
}

// Application Insights for telemetry
module applicationInsights 'br/public:avm/res/insights/component:0.4.2' = if (useMonitoring) {
module applicationInsights 'br/public:avm/res/insights/component:0.4.2' = if (useAppInsights) {
name: 'applicationinsights'
scope: resourceGroup
params: {
Expand Down Expand Up @@ -411,7 +423,7 @@ module openAiPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' =
}

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

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

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

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

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

// Azure Monitor Private Link Scope
module monitorPrivateLinkScope 'br/public:avm/res/insights/private-link-scope:0.7.1' = if (useVnet && useMonitoring) {
module monitorPrivateLinkScope 'br/public:avm/res/insights/private-link-scope:0.7.1' = if (useVnet && useAppInsights) {
name: 'monitor-private-link-scope'
scope: resourceGroup
params: {
Expand Down Expand Up @@ -654,7 +666,7 @@ module containerApps 'core/host/container-apps.bicep' = {
tags: tags
containerAppsEnvironmentName: '${prefix}-containerapps-env'
containerRegistryName: '${take(replace(prefix, '-', ''), 42)}registry'
logAnalyticsWorkspaceName: useMonitoring ? logAnalyticsWorkspace!.outputs.name : ''
logAnalyticsWorkspaceName: useAppInsights ? logAnalyticsWorkspace!.outputs.name : ''
// Reference the virtual network only if useVnet is true
subnetResourceId: useVnet ? virtualNetwork!.outputs.subnetResourceIds[0] : ''
vnetName: useVnet ? virtualNetwork!.outputs.name : ''
Expand Down Expand Up @@ -746,7 +758,8 @@ module server 'server.bicep' = {
cosmosDbContainer: cosmosDbContainerName
cosmosDbUserContainer: cosmosDbUserContainerName
cosmosDbOAuthContainer: cosmosDbOAuthContainerName
applicationInsightsConnectionString: useMonitoring ? applicationInsights!.outputs.connectionString : ''
applicationInsightsConnectionString: useAppInsights ? applicationInsights!.outputs.connectionString : ''
openTelemetryPlatform: openTelemetryPlatform
exists: serverExists
// Keycloak authentication configuration (only when enabled)
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/realms/${keycloakRealmName}' : ''
Expand All @@ -759,6 +772,7 @@ module server 'server.bicep' = {
entraProxyBaseUrl: useEntraProxy ? entraProxyMcpServerBaseUrl : ''
tenantId: useEntraProxy ? tenant().tenantId : ''
mcpAuthProvider: mcpAuthProvider
logfireToken: logfireToken
}
}

Expand Down Expand Up @@ -897,7 +911,7 @@ output AZURE_COSMOSDB_USER_CONTAINER string = cosmosDbUserContainerName
output AZURE_COSMOSDB_OAUTH_CONTAINER string = cosmosDbOAuthContainerName

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

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

// Auth provider for env scripts
output MCP_AUTH_PROVIDER string = mcpAuthProvider

// OpenTelemetry platform for env scripts
output OPENTELEMETRY_PLATFORM string = openTelemetryPlatform
8 changes: 5 additions & 3 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"principalId": {
"value": "${AZURE_PRINCIPAL_ID}"
},
"useMonitoring": {
"value": "${USE_MONITORING=true}"
"openTelemetryPlatform": {
"value": "${OPENTELEMETRY_PLATFORM=appinsights}"
},
"useVnet": {
"value": "${USE_VNET=false}"
Expand Down Expand Up @@ -50,12 +50,14 @@
"keycloakMcpServerAudience": {
"value": "${KEYCLOAK_MCP_SERVER_AUDIENCE=mcp-server}"
},

"entraProxyClientId": {
"value": "${ENTRA_PROXY_AZURE_CLIENT_ID}"
},
"entraProxyClientSecret": {
"value": "${ENTRA_PROXY_AZURE_CLIENT_SECRET}"
},
"logfireToken": {
"value": "${LOGFIRE_TOKEN}"
}
}
}
Loading