-
Notifications
You must be signed in to change notification settings - Fork 369
Adding Foundry Agent MCP Private Connectivity Lab #256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 187dcf7
feat: add AI Foundry private MCP connectivity lab
pablocast be415fa
feat: add AI Foundry private MCP connectivity lab
pablocast f1f4066
feat: add AI Foundry private MCP connectivity lab
pablocast e24a73e
feat: add AI Foundry private MCP connectivity lab
pablocast 81e195f
feat: add AI Foundry private MCP connectivity lab
pablocast 87aa499
feat: add AI Foundry private MCP connectivity lab
pablocast 6db448c
feat: add AI Foundry private MCP connectivity lab
pablocast 2b60b72
feat: add AI Foundry private MCP connectivity lab
pablocast 19acfd2
feat: add AI Foundry private MCP connectivity lab
pablocast 007bc07
Merge pull request #1 from pablocast/labs/ai-foundry-private-mcp
pablocast 9e47c08
feat: add AI Foundry private MCP connectivity lab
pablocast 9d5c943
Fixing errors and improving compatibility with python on linux systems.
nourshaker-msft c7f9aa7
bringing clean up resources script in line with other labs
nourshaker-msft 4e22f00
Final modificatins before starting on the PE deployment for Foundry
nourshaker-msft dda7cba
Merge pull request #2 from nourshaker-msft/main
pablocast 3638e37
New image adding MCP private endpoint diagram and updating notebook f…
nourshaker-msft f01d6b0
Merge pull request #3 from nourshaker-msft/main
pablocast File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | ||
|
|
||
|  | ||
|
|
||
| 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 | ||
|
|
||
|  | ||
|
|
||
| ##### Test 2: Unauthorized MCP Access | ||
|
|
||
| - Attempt to call the MCP server without an authorization token | ||
| - Expected Result: 401 Unauthorized, validating OAuth enforcement | ||
|
|
||
|  | ||
|
|
||
| ##### 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 | ||
|
|
||
|  | ||
|
|
||
| ## 🧹 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/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}") | ||
|
|
||
| print("Environment variables loaded successfully!") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
There was a problem hiding this comment.
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