Skip to content

Commit b7798dd

Browse files
Merge pull request #256 from pablocast/main
Adding Foundry Agent MCP Private Connectivity Lab
2 parents 14bc489 + f01d6b0 commit b7798dd

25 files changed

+2835
-0
lines changed
175 KB
Loading
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Azure AI Foundry Private Connectivity Lab
2+
3+
A hands-on lab demonstrating secure agent consumption through private networking with Azure AI Foundry, API Management, and Front Door. Features MCP (Model Context Protocol) integration, private endpoints, and agent-based interactions. MCP servers are only accessible through the frontdoor.
4+
5+
6+
## 🏗️ Architecture
7+
8+
This lab demonstrates a fully private AI infrastructure:
9+
10+
![architecture](architecture.png)
11+
12+
Core Components:
13+
- **Azure AI Foundry**: AI Services account with private endpoint access only
14+
- **Azure API Management (APIM)**: Deployed in VNet, manages traffic to AI services via private endpoints
15+
- **Azure Front Door**: Premium tier with Private Link to APIM - the only publicly accessible endpoint
16+
- **Azure Key Vault**: Stores secrets, accessible only from private network
17+
- **Jumpbox VM**: Ubuntu VM with managed identity for testing from private network
18+
- **MCP Integration**: Model Context Protocol server exposed through APIM
19+
20+
**Network Flow**: Client → Front Door (Public) → Private Link → APIM (Private VNet) → Private Endpoint → AI Foundry
21+
22+
## 📋 Prerequisites
23+
24+
- [Python 3.11 or later](https://www.python.org/) installed locally
25+
- [VS Code](https://code.visualstudio.com/) with [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) enabled
26+
- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and authenticated
27+
- [An Azure Subscription](https://azure.microsoft.com/free/) with Contributor permissions
28+
29+
## 🚀 Deployment
30+
31+
### 1. Clone and Setup
32+
33+
```bash
34+
git clone <repository-url>
35+
cd AI-Gateway
36+
pip install -r requirements.txt
37+
```
38+
39+
### 2. Deploy Infrastructure
40+
41+
Open the [Jupyter notebook](./foundry-private-mcp.ipynb) and execute the cells to:
42+
43+
1. **Initialize variables** - Set deployment name, locations, and model configurations
44+
2. **Create resource group** - Provision the Azure resource group
45+
3. **Deploy Bicep template** - Deploy all infrastructure (15-20 minutes):
46+
- Virtual Network with subnets
47+
- Azure AI Foundry with private endpoint
48+
- APIM integrated in VNet
49+
- Front Door with Private Link
50+
- Key Vault with secrets
51+
- Jumpbox VM with managed identity
52+
- MCP API configuration
53+
54+
4. **Approve Private Link** - Approve Front Door connection to APIM
55+
5. **Disable APIM public access** - Lock down APIM to private network only
56+
57+
### 3. Verify Deployment Outputs
58+
59+
The deployment provides:
60+
- `frontDoorEndpointHostName` - Public endpoint for testing
61+
- `apimResourceGatewayURL` - Private APIM URL (inaccessible from internet)
62+
- `keyVaultUrl` - Key Vault URL for secrets
63+
- `aiFoundryProjectEndpoint` - AI Foundry project endpoint
64+
65+
## 🧪 Testing from Jumpbox
66+
67+
### Connect to Jumpbox
68+
69+
Use Azure Bastion to connect to the VM:
70+
71+
```bash
72+
az network bastion ssh \
73+
--name bastion-host \
74+
--resource-group <resource-group> \
75+
--target-resource-id <vm-resource-id> \
76+
--auth-type password \
77+
--username azureuser
78+
```
79+
80+
Or connect via the Azure Portal: Navigate to the VM → Connect → Bastion
81+
82+
### Create Python Scripts
83+
84+
Once connected to the jumpbox, create the required scripts:
85+
86+
#### 1. Create `scripts/load_env_from_kv.py`
87+
```bash
88+
cat > ~/scripts/load_env_from_kv.py << 'EOF'
89+
# Paste content of agent/load_env_from_kv.py here
90+
EOF
91+
```
92+
93+
#### 2. Create `scripts/sample_agents_mcp.py`
94+
```bash
95+
cat > ~/scripts/sample_agents_mcp.py << 'EOF'
96+
# Paste content of agent/sample_agents_mcp.py here
97+
EOF
98+
```
99+
100+
### Run the Agent
101+
102+
```bash
103+
# Activate virtual environment
104+
source ~/venv/bin/activate
105+
106+
# Get Key Vault URL from deployment outputs
107+
KEY_VAULT_URL="https://kv-xxxxx.vault.azure.net/"
108+
109+
# Run the MCP agent
110+
python3 ~/scripts/sample_agents_mcp.py $KEY_VAULT_URL
111+
```
112+
113+
The script will:
114+
1. Load secrets from Key Vault using managed identity
115+
2. Create an AI agent with MCP tools
116+
3. Send a test message ("Order sku-123 with 5 items")
117+
4. Process MCP tool calls through APIM
118+
5. Display the conversation and results
119+
120+
### Test the Agent
121+
122+
Now you can test the agent:
123+
124+
#### Test 1: Public Access to APIM
125+
126+
- Access the APIM endpoint directly through its public URL
127+
- Expected Result: Request should be blocked or restricted, demonstrating that direct APIM access is protected
128+
129+
![Execution Evidence](./docs/unauthorized_apim.png)
130+
131+
##### Test 2: Unauthorized MCP Access
132+
133+
- Attempt to call the MCP server without an authorization token
134+
- Expected Result: 401 Unauthorized, validating OAuth enforcement
135+
136+
![Execution Evidence](./docs/unauthorized_mcp.png)
137+
138+
##### Test 3: Authorized Access via Front Door
139+
140+
- Access the MCP server through Front Door with a valid authorization token
141+
- Expected Result: Successful response, demonstrating the complete secure flow: Client → Front Door → Private Link → APIM → MCP Server
142+
143+
![Execution Evidence](./docs/execution_evidence.png)
144+
145+
## 🧹 Clean Up Resources
146+
147+
When finished, delete all resources to avoid charges: <br>
148+
Use [clean-up-resources notebook](./clean-up-resources.ipynb).
149+
150+
## 📚 Additional Resources
151+
152+
- [Azure API Management Private Endpoints](https://learn.microsoft.com/azure/api-management/private-endpoint)
153+
- [Azure Front Door Private Link](https://learn.microsoft.com/azure/frontdoor/private-link)
154+
- [Azure AI Foundry Documentation](https://learn.microsoft.com/azure/ai-services/)
155+
- [Model Context Protocol](https://modelcontextprotocol.io/)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import os
2+
from azure.identity import DefaultAzureCredential
3+
from azure.keyvault.secrets import SecretClient
4+
5+
def load_secrets_from_keyvault(vault_url: str):
6+
"""Load secrets from Azure Key Vault and set as environment variables"""
7+
credential = DefaultAzureCredential()
8+
kv_client = SecretClient(vault_url=vault_url, credential=credential)
9+
10+
secret_names = [
11+
"MCP-SERVER-URL",
12+
"MCP-SERVER-LABEL",
13+
"AZURE-AI-PROJECT-ENDPOINT",
14+
"AZURE-AI-MODEL-DEPLOYMENT-NAME"
15+
]
16+
17+
print("Loading secrets from Key Vault...")
18+
for secret_name in secret_names:
19+
try:
20+
secret = kv_client.get_secret(secret_name)
21+
env_var_name = secret_name.replace("-", "_")
22+
os.environ[env_var_name] = secret.value
23+
except Exception as e:
24+
print(f"Error loading secret {secret_name}: {e}")
25+
26+
print("Environment variables loaded successfully!")
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import os, time
2+
from azure.ai.projects import AIProjectClient
3+
from azure.identity import DefaultAzureCredential
4+
from azure.ai.agents.models import (
5+
ListSortOrder,
6+
McpTool,
7+
RequiredMcpToolCall,
8+
RunStepActivityDetails,
9+
SubmitToolApprovalAction,
10+
ToolApproval,
11+
)
12+
from load_env_from_kv import load_secrets_from_keyvault
13+
14+
def run_agent(key_vault_url):
15+
"""Run the MCP agent with the given Key Vault URL"""
16+
17+
# Load secrets from Key Vault
18+
if key_vault_url:
19+
load_secrets_from_keyvault(key_vault_url)
20+
else:
21+
raise Exception("KEY_VAULT_URL is required")
22+
23+
# Get MCP server configuration from environment variables
24+
mcp_server_url = os.environ.get("MCP_SERVER_URL")
25+
mcp_server_label = os.environ.get("MCP_SERVER_LABEL")
26+
27+
project_client = AIProjectClient(
28+
endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
29+
credential=DefaultAzureCredential(),
30+
)
31+
32+
mcp_tool = McpTool(
33+
server_label=mcp_server_label,
34+
server_url=mcp_server_url,
35+
allowed_tools=[],
36+
)
37+
38+
# Get access token using managed identity
39+
credential = DefaultAzureCredential()
40+
access_token = credential.get_token("https://azure-api.net/authorization-manager/.default").token
41+
print("Obtained access token for MCP server.")
42+
43+
# Create agent with MCP tool and process agent run
44+
with project_client:
45+
agents_client = project_client.agents
46+
47+
# Create a new agent
48+
agent = agents_client.create_agent(
49+
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
50+
name="my-mcp-agent",
51+
instructions="You are a helpful agent that can use MCP tools to assist users. Use the available MCP tools to answer questions and perform tasks.",
52+
tools=mcp_tool.definitions,
53+
)
54+
55+
print(f"Created agent, ID: {agent.id}")
56+
print(f"MCP Server: {mcp_tool.server_label} at {mcp_tool.server_url}")
57+
58+
# Create thread for communication
59+
thread = agents_client.threads.create()
60+
print(f"Created thread, ID: {thread.id}")
61+
62+
# Create message to thread
63+
message = agents_client.messages.create(
64+
thread_id=thread.id,
65+
role="user",
66+
content="Order sku-123 with 5 items",
67+
)
68+
print(f"Created message, ID: {message.id}")
69+
70+
# Create and process agent run in thread with MCP tools
71+
mcp_tool.update_headers("Authorization", f"Bearer {access_token}")
72+
run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id, tool_resources=mcp_tool.resources)
73+
print(f"Created run, ID: {run.id}")
74+
75+
while run.status in ["queued", "in_progress", "requires_action"]:
76+
time.sleep(0.1)
77+
run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)
78+
79+
if run.status == "requires_action" and isinstance(run.required_action, SubmitToolApprovalAction):
80+
tool_calls = run.required_action.submit_tool_approval.tool_calls
81+
if not tool_calls:
82+
print("No tool calls provided - cancelling run")
83+
agents_client.runs.cancel(thread_id=thread.id, run_id=run.id)
84+
break
85+
86+
tool_approvals = []
87+
for tool_call in tool_calls:
88+
if isinstance(tool_call, RequiredMcpToolCall):
89+
try:
90+
print(f"Approving tool call: {tool_call}")
91+
tool_approvals.append(
92+
ToolApproval(
93+
tool_call_id=tool_call.id,
94+
approve=True,
95+
headers=mcp_tool.headers,
96+
)
97+
)
98+
except Exception as e:
99+
print(f"Error approving tool_call {tool_call.id}: {e}")
100+
101+
if tool_approvals:
102+
agents_client.runs.submit_tool_outputs(
103+
thread_id=thread.id, run_id=run.id, tool_approvals=tool_approvals
104+
)
105+
106+
print(f"Current run status: {run.status}")
107+
108+
print(f"Run completed with status: {run.status}")
109+
if run.status == "failed":
110+
print(f"Run failed: {run.last_error}")
111+
112+
# Display run steps and tool calls
113+
run_steps = agents_client.run_steps.list(thread_id=thread.id, run_id=run.id)
114+
115+
# Loop through each step
116+
for step in run_steps:
117+
print(f"Step {step['id']} status: {step['status']}")
118+
119+
# Check if there are tool calls in the step details
120+
step_details = step.get("step_details", {})
121+
tool_calls = step_details.get("tool_calls", [])
122+
123+
if tool_calls:
124+
print(" MCP Tool calls:")
125+
for call in tool_calls:
126+
print(f" Tool Call ID: {call.get('id')}")
127+
print(f" Type: {call.get('type')}")
128+
129+
if isinstance(step_details, RunStepActivityDetails):
130+
for activity in step_details.activities:
131+
for function_name, function_definition in activity.tools.items():
132+
print(
133+
f' The function {function_name} with description "{function_definition.description}" will be called.:'
134+
)
135+
if len(function_definition.parameters) > 0:
136+
print(" Function parameters:")
137+
for argument, func_argument in function_definition.parameters.properties.items():
138+
print(f" {argument}")
139+
print(f" Type: {func_argument.type}")
140+
print(f" Description: {func_argument.description}")
141+
else:
142+
print("This function has no parameters")
143+
144+
print() # add an extra newline between steps
145+
146+
# Fetch and log all messages
147+
messages = agents_client.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
148+
print("\nConversation:")
149+
print("-" * 50)
150+
for msg in messages:
151+
if msg.text_messages:
152+
last_text = msg.text_messages[-1]
153+
print(f"{msg.role.upper()}: {last_text.text.value}")
154+
print("-" * 50)
155+
156+
# Example of dynamic tool management
157+
print(f"\nDemonstrating dynamic tool management:")
158+
print(f"Current allowed tools: {mcp_tool.allowed_tools}")
159+
160+
# Clean-up and delete the agent once the run is finished
161+
agents_client.delete_agent(agent.id)
162+
print("Deleted agent")
163+
164+
165+
if __name__ == "__main__":
166+
import sys
167+
168+
# Get Key Vault URL from command line argument or environment variable
169+
if len(sys.argv) > 1:
170+
key_vault_url = sys.argv[1]
171+
else:
172+
key_vault_url = os.environ.get("KEY_VAULT_URL")
173+
174+
if not key_vault_url:
175+
print("Error: KEY_VAULT_URL must be provided as argument or environment variable")
176+
print("Usage: python sample_agents_mcp.py <key-vault-url>")
177+
sys.exit(1)
178+
179+
print(f"Using Key Vault: {key_vault_url}")
180+
run_agent(key_vault_url)
71.2 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"experimentalFeaturesEnabled": {
3+
"extensibility": true
4+
},
5+
"extensions": {
6+
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.2.0-preview"
7+
}
8+
}

0 commit comments

Comments
 (0)