Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4be4a52
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
187dcf7
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
be415fa
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
f1f4066
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
e24a73e
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
81e195f
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
87aa499
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
6db448c
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
2b60b72
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
19acfd2
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
007bc07
Merge pull request #1 from pablocast/labs/ai-foundry-private-mcp
pablocast Dec 17, 2025
9e47c08
feat: add AI Foundry private MCP connectivity lab
pablocast Dec 17, 2025
9d5c943
Fixing errors and improving compatibility with python on linux systems.
nourshaker-msft Dec 22, 2025
c7f9aa7
bringing clean up resources script in line with other labs
nourshaker-msft Dec 22, 2025
4e22f00
Final modificatins before starting on the PE deployment for Foundry
nourshaker-msft Dec 22, 2025
dda7cba
Merge pull request #2 from nourshaker-msft/main
pablocast Dec 22, 2025
3638e37
New image adding MCP private endpoint diagram and updating notebook f…
nourshaker-msft Dec 22, 2025
f01d6b0
Merge pull request #3 from nourshaker-msft/main
pablocast Dec 22, 2025
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
155 changes: 155 additions & 0 deletions labs/ai-foundry-private-mcp/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Azure AI Foundry Private Connectivity Lab

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.


## 🏗️ Architecture

This lab demonstrates a fully private AI infrastructure:

![architecture](architecture.png)

Core Components:
- **Azure AI Foundry**: AI Services account with private endpoint access only
- **Azure API Management (APIM)**: Deployed in VNet, manages traffic to AI services via private endpoints
- **Azure Front Door**: Premium tier with Private Link to APIM - the only publicly accessible endpoint
- **Azure Key Vault**: Stores secrets, accessible only from private network
- **Jumpbox VM**: Ubuntu VM with managed identity for testing from private network
- **MCP Integration**: Model Context Protocol server exposed through APIM

**Network Flow**: Client → Front Door (Public) → Private Link → APIM (Private VNet) → Private Endpoint → AI Foundry

## 📋 Prerequisites

- [Python 3.11 or later](https://www.python.org/) installed locally
- [VS Code](https://code.visualstudio.com/) with [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) enabled
- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and authenticated
- [An Azure Subscription](https://azure.microsoft.com/free/) with Contributor permissions

## 🚀 Deployment

### 1. Clone and Setup

```bash
git clone <repository-url>
cd AI-Gateway
pip install -r requirements.txt
```

### 2. Deploy Infrastructure

Open the [Jupyter notebook](./foundry-private-mcp.ipynb) and execute the cells to:

1. **Initialize variables** - Set deployment name, locations, and model configurations
2. **Create resource group** - Provision the Azure resource group
3. **Deploy Bicep template** - Deploy all infrastructure (15-20 minutes):
- Virtual Network with subnets
- Azure AI Foundry with private endpoint
- APIM integrated in VNet
- Front Door with Private Link
- Key Vault with secrets
- Jumpbox VM with managed identity
- MCP API configuration

4. **Approve Private Link** - Approve Front Door connection to APIM
5. **Disable APIM public access** - Lock down APIM to private network only

### 3. Verify Deployment Outputs

The deployment provides:
- `frontDoorEndpointHostName` - Public endpoint for testing
- `apimResourceGatewayURL` - Private APIM URL (inaccessible from internet)
- `keyVaultUrl` - Key Vault URL for secrets
- `aiFoundryProjectEndpoint` - AI Foundry project endpoint

## 🧪 Testing from Jumpbox

### Connect to Jumpbox

Use Azure Bastion to connect to the VM:

```bash
az network bastion ssh \
--name bastion-host \
--resource-group <resource-group> \
--target-resource-id <vm-resource-id> \
--auth-type password \
--username azureuser
```

Or connect via the Azure Portal: Navigate to the VM → Connect → Bastion

### Create Python Scripts

Once connected to the jumpbox, create the required scripts:

#### 1. Create `scripts/load_env_from_kv.py`
```bash
cat > ~/scripts/load_env_from_kv.py << 'EOF'
# Paste content of agent/load_env_from_kv.py here
EOF
```

#### 2. Create `scripts/sample_agents_mcp.py`
```bash
cat > ~/scripts/sample_agents_mcp.py << 'EOF'
# Paste content of agent/sample_agents_mcp.py here
EOF
```

### Run the Agent

```bash
# Activate virtual environment
source ~/venv/bin/activate

# Get Key Vault URL from deployment outputs
KEY_VAULT_URL="https://kv-xxxxx.vault.azure.net/"

# Run the MCP agent
python3 ~/scripts/sample_agents_mcp.py $KEY_VAULT_URL
```

The script will:
1. Load secrets from Key Vault using managed identity
2. Create an AI agent with MCP tools
3. Send a test message ("Order sku-123 with 5 items")
4. Process MCP tool calls through APIM
5. Display the conversation and results

### Test the Agent

Now you can test the agent:

#### Test 1: Public Access to APIM

- Access the APIM endpoint directly through its public URL
- Expected Result: Request should be blocked or restricted, demonstrating that direct APIM access is protected

![Execution Evidence](./docs/unauthorized_apim.png)

##### Test 2: Unauthorized MCP Access

- Attempt to call the MCP server without an authorization token
- Expected Result: 401 Unauthorized, validating OAuth enforcement

![Execution Evidence](./docs/unauthorized_mcp.png)

##### Test 3: Authorized Access via Front Door

- Access the MCP server through Front Door with a valid authorization token
- Expected Result: Successful response, demonstrating the complete secure flow: Client → Front Door → Private Link → APIM → MCP Server

![Execution Evidence](./docs/execution_evidence.png)

## 🧹 Clean Up Resources

When finished, delete all resources to avoid charges: <br>
Use [clean-up-resources notebook](./clean-up-resources.ipynb).

## 📚 Additional Resources

- [Azure API Management Private Endpoints](https://learn.microsoft.com/azure/api-management/private-endpoint)
- [Azure Front Door Private Link](https://learn.microsoft.com/azure/frontdoor/private-link)
- [Azure AI Foundry Documentation](https://learn.microsoft.com/azure/ai-services/)
- [Model Context Protocol](https://modelcontextprotocol.io/)
26 changes: 26 additions & 0 deletions labs/ai-foundry-private-mcp/agent/load_env_from_kv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

def load_secrets_from_keyvault(vault_url: str):
"""Load secrets from Azure Key Vault and set as environment variables"""
credential = DefaultAzureCredential()
kv_client = SecretClient(vault_url=vault_url, credential=credential)

secret_names = [
"MCP-SERVER-URL",
"MCP-SERVER-LABEL",
"AZURE-AI-PROJECT-ENDPOINT",
"AZURE-AI-MODEL-DEPLOYMENT-NAME"
]

print("Loading secrets from Key Vault...")
for secret_name in secret_names:
try:
secret = kv_client.get_secret(secret_name)
env_var_name = secret_name.replace("-", "_")
os.environ[env_var_name] = secret.value
except Exception as e:
print(f"Error loading secret {secret_name}: {e}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not printing out the secret value, just the secret key to help troubleshooting in a test environment


print("Environment variables loaded successfully!")
180 changes: 180 additions & 0 deletions labs/ai-foundry-private-mcp/agent/sample_agents_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import os, time
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential
from azure.ai.agents.models import (
ListSortOrder,
McpTool,
RequiredMcpToolCall,
RunStepActivityDetails,
SubmitToolApprovalAction,
ToolApproval,
)
from load_env_from_kv import load_secrets_from_keyvault

def run_agent(key_vault_url):
"""Run the MCP agent with the given Key Vault URL"""

# Load secrets from Key Vault
if key_vault_url:
load_secrets_from_keyvault(key_vault_url)
else:
raise Exception("KEY_VAULT_URL is required")

# Get MCP server configuration from environment variables
mcp_server_url = os.environ.get("MCP_SERVER_URL")
mcp_server_label = os.environ.get("MCP_SERVER_LABEL")

project_client = AIProjectClient(
endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)

mcp_tool = McpTool(
server_label=mcp_server_label,
server_url=mcp_server_url,
allowed_tools=[],
)

# Get access token using managed identity
credential = DefaultAzureCredential()
access_token = credential.get_token("https://azure-api.net/authorization-manager/.default").token
print("Obtained access token for MCP server.")

# Create agent with MCP tool and process agent run
with project_client:
agents_client = project_client.agents

# Create a new agent
agent = agents_client.create_agent(
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
name="my-mcp-agent",
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.",
tools=mcp_tool.definitions,
)

print(f"Created agent, ID: {agent.id}")
print(f"MCP Server: {mcp_tool.server_label} at {mcp_tool.server_url}")

# Create thread for communication
thread = agents_client.threads.create()
print(f"Created thread, ID: {thread.id}")

# Create message to thread
message = agents_client.messages.create(
thread_id=thread.id,
role="user",
content="Order sku-123 with 5 items",
)
print(f"Created message, ID: {message.id}")

# Create and process agent run in thread with MCP tools
mcp_tool.update_headers("Authorization", f"Bearer {access_token}")
run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id, tool_resources=mcp_tool.resources)
print(f"Created run, ID: {run.id}")

while run.status in ["queued", "in_progress", "requires_action"]:
time.sleep(0.1)
run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)

if run.status == "requires_action" and isinstance(run.required_action, SubmitToolApprovalAction):
tool_calls = run.required_action.submit_tool_approval.tool_calls
if not tool_calls:
print("No tool calls provided - cancelling run")
agents_client.runs.cancel(thread_id=thread.id, run_id=run.id)
break

tool_approvals = []
for tool_call in tool_calls:
if isinstance(tool_call, RequiredMcpToolCall):
try:
print(f"Approving tool call: {tool_call}")
tool_approvals.append(
ToolApproval(
tool_call_id=tool_call.id,
approve=True,
headers=mcp_tool.headers,
)
)
except Exception as e:
print(f"Error approving tool_call {tool_call.id}: {e}")

if tool_approvals:
agents_client.runs.submit_tool_outputs(
thread_id=thread.id, run_id=run.id, tool_approvals=tool_approvals
)

print(f"Current run status: {run.status}")

print(f"Run completed with status: {run.status}")
if run.status == "failed":
print(f"Run failed: {run.last_error}")

# Display run steps and tool calls
run_steps = agents_client.run_steps.list(thread_id=thread.id, run_id=run.id)

# Loop through each step
for step in run_steps:
print(f"Step {step['id']} status: {step['status']}")

# Check if there are tool calls in the step details
step_details = step.get("step_details", {})
tool_calls = step_details.get("tool_calls", [])

if tool_calls:
print(" MCP Tool calls:")
for call in tool_calls:
print(f" Tool Call ID: {call.get('id')}")
print(f" Type: {call.get('type')}")

if isinstance(step_details, RunStepActivityDetails):
for activity in step_details.activities:
for function_name, function_definition in activity.tools.items():
print(
f' The function {function_name} with description "{function_definition.description}" will be called.:'
)
if len(function_definition.parameters) > 0:
print(" Function parameters:")
for argument, func_argument in function_definition.parameters.properties.items():
print(f" {argument}")
print(f" Type: {func_argument.type}")
print(f" Description: {func_argument.description}")
else:
print("This function has no parameters")

print() # add an extra newline between steps

# Fetch and log all messages
messages = agents_client.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
print("\nConversation:")
print("-" * 50)
for msg in messages:
if msg.text_messages:
last_text = msg.text_messages[-1]
print(f"{msg.role.upper()}: {last_text.text.value}")
print("-" * 50)

# Example of dynamic tool management
print(f"\nDemonstrating dynamic tool management:")
print(f"Current allowed tools: {mcp_tool.allowed_tools}")

# Clean-up and delete the agent once the run is finished
agents_client.delete_agent(agent.id)
print("Deleted agent")


if __name__ == "__main__":
import sys

# Get Key Vault URL from command line argument or environment variable
if len(sys.argv) > 1:
key_vault_url = sys.argv[1]
else:
key_vault_url = os.environ.get("KEY_VAULT_URL")

if not key_vault_url:
print("Error: KEY_VAULT_URL must be provided as argument or environment variable")
print("Usage: python sample_agents_mcp.py <key-vault-url>")
sys.exit(1)

print(f"Using Key Vault: {key_vault_url}")
run_agent(key_vault_url)
Binary file added labs/ai-foundry-private-mcp/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions labs/ai-foundry-private-mcp/bicepconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"experimentalFeaturesEnabled": {
"extensibility": true
},
"extensions": {
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.2.0-preview"
}
}
Loading
Loading