|
9 | 9 | "## Microsoft Foundry Private connectivity lab\n", |
10 | 10 | "\n", |
11 | 11 | "\n", |
12 | | - "Playground to show how to create a private network for consuming REST API managed as MCPs using `Foundry Agents V1`. This lab demonstrates how to create a private network for consuming MCPs from `Foundry Agents V1` using `Private Link Services`, `Azure API Management (APIM)`, and `Azure Front Door`.\n", |
| 12 | + "Playground to show how to create a private network for consuming REST API managed as MCPs using `Foundry Agents Classic`. This lab demonstrates how to create a private network for consuming MCPs from `Foundry Agents Classic` using `Private Link Services`, `Azure API Management (APIM)`, and `Azure Front Door`.\n", |
13 | 13 | "\n", |
14 | 14 | "Notes:\n", |
15 | 15 | "- `Foundry Agents` are only accessible through `Private Endpoints`. Public network access is disabled.\n", |
|
19 | 19 | "- **`Azure Front Door` is the only publicly-accessible service.**\n", |
20 | 20 | "\n", |
21 | 21 | "### Prerequisites\n", |
| 22 | + "- **important** Admin access to be able to deploy and configure Enterprise Applications in Entra ID\n", |
22 | 23 | "- [Python 3.12 or later version](https://www.python.org/) installed\n", |
23 | 24 | "- [Pandas Library](https://pandas.pydata.org/) and matplotlib installed\n", |
24 | 25 | "- [VS Code](https://code.visualstudio.com/) installed with the [Jupyter notebook extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) enabled\n", |
|
160 | 161 | "if output.success and output.json_data:\n", |
161 | 162 | " apim_resource_id = utils.get_deployment_output(output, 'apimResourceId', 'apimResourceId')\n", |
162 | 163 | "\n", |
163 | | - " outputPls = utils.run(f\"az network private-endpoint-connection list --id {apim_resource_id} --query [?properties.privateLinkServiceConnectionState.status=='Pending'].id --output tsv\")\n", |
| 164 | + " outputPls = utils.run(f\"az network private-endpoint-connection list --id {apim_resource_id} --query \\\"[?properties.privateLinkServiceConnectionState.status=='Pending'].id\\\" --output tsv\")\n", |
164 | 165 | "\n", |
165 | 166 | " if outputPls.success:\n", |
166 | | - " pls_connection_id = outputPls.text\n", |
| 167 | + " pls_connection_id = outputPls.text.strip()\n", |
167 | 168 | " print(pls_connection_id)\n", |
168 | 169 | " utils.run(f\"az network private-endpoint-connection approve --id {pls_connection_id} --description 'Approved'\", f\"Private Link Connection approved.\", f\"Failed to approve Private Link Connection: {pls_connection_id}\")" |
169 | 170 | ] |
|
219 | 220 | " apim_resource_gateway_url = utils.get_deployment_output(output, 'apimResourceGatewayURL', 'APIM API Gateway URL')\n", |
220 | 221 | " apim_subscription_key = utils.get_deployment_output(output, 'apimSubscriptionKey', 'APIM Subscription Key (masked)', True)\n", |
221 | 222 | " ai_foundry_project_endpoint = utils.get_deployment_output(output, 'ai_project_endpoint', 'AI Foundry Project Endpoint')\n", |
222 | | - " ai_deployment_name = utils.get_deployment_output(output, 'ai_model_deployment_name', 'AI Model Deployment Name')" |
| 223 | + " ai_deployment_name = utils.get_deployment_output(output, 'ai_model_deployment_name', 'AI Model Deployment Name')\n", |
| 224 | + " vm_resource_id = utils.get_deployment_output(output, 'vmResourceId', 'VM Resource ID')\n", |
| 225 | + " key_vault_url = utils.get_deployment_output(output, 'keyVaultUrl', 'Key Vault URL')" |
223 | 226 | ] |
224 | 227 | }, |
225 | 228 | { |
|
229 | 232 | "<a id='6'></a>\n", |
230 | 233 | "### 6️⃣ 🧪 Test the API using a direct HTTP call through Frontdoor\n", |
231 | 234 | "\n", |
232 | | - "Requests is an elegant and simple HTTP library for Python that will be used here to make raw API requests and inspect the responses. " |
| 235 | + "Requests is an elegant and simple HTTP library for Python that will be used here to make raw API requests and inspect the responses.\n", |
| 236 | + "\n", |
| 237 | + "**After a successful deployment it may take up to 45 mins for this cell to run correctly**" |
233 | 238 | ] |
234 | 239 | }, |
235 | 240 | { |
|
245 | 250 | " request = {\"method\":\"tools/call\",\"params\":{\"name\":\"PlaceOrder-invoke\",\"arguments\":{\"request-PlaceOrder\":{\"sku\":\"sku-123\",\"quantity\":5}}},\"jsonrpc\":\"2.0\",\"id\":1}\n", |
246 | 251 | " print(f\"Calling MCP endpoint at URL: {url}\")\n", |
247 | 252 | " utils.print_info(\"Calling MCP endpoint WITH authorization...\")\n", |
248 | | - " output = utils.run(\"az account get-access-token --resource \\\"https://azure-api.net/authorization-manager\\\"\")\n", |
| 253 | + " output = utils.run('az account get-access-token --resource \"https://azure-api.net/authorization-manager\"')\n", |
249 | 254 | "\n", |
250 | 255 | " access_token = output.json_data['accessToken']\n", |
251 | 256 | " response = requests.post(url, stream=True, headers={\"Content-Type\": \"application/json\", \"agent-id\": \"Agent1\", \"Authorization\": \"Bearer \" + str(access_token)}, json=request)\n", |
| 257 | + " print(response.status_code)\n", |
252 | 258 | "\n", |
253 | 259 | " print(f\"Response status code: {response.status_code}\")\n", |
254 | 260 | "\n", |
|
265 | 271 | " data = json.loads(line.strip()[5:])\n", |
266 | 272 | " print(json.dumps(json.loads(data[\"result\"][\"content\"][0][\"text\"]), indent=4))\n", |
267 | 273 | " else:\n", |
268 | | - " utils.print_error(f\"Unexpected status code: {response.status_code}. Response text: {response.text}\")\n", |
| 274 | + " try:\n", |
| 275 | + " error_text = response.text\n", |
| 276 | + " except Exception:\n", |
| 277 | + " error_text = \"[Unable to read response body]\"\n", |
| 278 | + " utils.print_error(f\"Unexpected status code: {response.status_code}.\")\n", |
269 | 279 | "\n", |
270 | 280 | "call_mcp_api(f\"https://{frontdoor_endpoint}\")" |
271 | 281 | ] |
|
304 | 314 | "\n" |
305 | 315 | ] |
306 | 316 | }, |
| 317 | + { |
| 318 | + "cell_type": "code", |
| 319 | + "execution_count": null, |
| 320 | + "metadata": {}, |
| 321 | + "outputs": [], |
| 322 | + "source": [ |
| 323 | + "utils.print_info(f\"\"\"Run the following command to connect to the VM via Bastion: \n", |
| 324 | + "az network bastion ssh \\\n", |
| 325 | + "--name bastion-host \\\n", |
| 326 | + "--resource-group {resource_group_name} \\\n", |
| 327 | + "--target-resource-id {vm_resource_id} \\\n", |
| 328 | + "--auth-type password \\\n", |
| 329 | + "--username azureuser\n", |
| 330 | + "\"\"\")" |
| 331 | + ] |
| 332 | + }, |
307 | 333 | { |
308 | 334 | "cell_type": "markdown", |
309 | 335 | "metadata": {}, |
310 | 336 | "source": [ |
311 | 337 | "##### 1. Connect to Jumpbox\n", |
312 | | - "Use Azure Bastion to connect to the VM:\n", |
| 338 | + "Use Azure Bastion to connect to the VM (full command is the output from previos cell):\n", |
313 | 339 | "\n", |
314 | 340 | "```bash\n", |
315 | 341 | "az network bastion ssh \\\n", |
|
320 | 346 | " --username azureuser\n", |
321 | 347 | "```\n", |
322 | 348 | "\n", |
| 349 | + "***Password:***\n", |
| 350 | + "```bash\n", |
| 351 | + "@Aa123456789\n", |
| 352 | + "```\n", |
| 353 | + "\n", |
323 | 354 | "Or connect via the Azure Portal: Navigate to the VM → Connect → Bastion\n", |
324 | 355 | "\n", |
325 | 356 | "Once connected to the jumpbox, create the required scripts:\n", |
326 | 357 | "\n", |
327 | | - "##### 2. Create `scripts/load_env_from_kv.py`\n", |
| 358 | + "##### 2. Download needed files\n", |
328 | 359 | "```bash \n", |
329 | | - "cat > ~/scripts/load_env_from_kv.py << 'EOF'\n", |
330 | | - "# Paste content of agent/load_env_from_kv.py here\n", |
331 | | - "EOF\n", |
| 360 | + "wget https://raw.githubusercontent.com/pablocast/AI-Gateway/refs/heads/main/labs/ai-foundry-private-mcp/agent/load_env_from_kv.py\n", |
| 361 | + "wget https://raw.githubusercontent.com/pablocast/AI-Gateway/refs/heads/main/labs/ai-foundry-private-mcp/agent/sample_agents_mcp.py\n", |
332 | 362 | "```\n", |
333 | 363 | "\n", |
334 | | - "\n", |
335 | | - "##### 3. Create `scripts/sample_agents_mcp.py`\n", |
336 | | - "```bash \n", |
337 | | - "cat > ~/scripts/sample_agents_mcp.py << 'EOF'\n", |
338 | | - "# Paste content of agent/sample_agents_mcp.py here\n", |
339 | | - "EOF\n", |
340 | | - "```\n", |
341 | | - "\n", |
342 | | - "##### 4. Run the agent\n", |
| 364 | + "##### 3. Run the agent\n", |
343 | 365 | "```bash\n", |
344 | | - "# Activate virtual environment\n", |
345 | | - "source ~/venv/bin/activate\n", |
346 | | - "\n", |
347 | 366 | "# Get Key Vault URL from deployment outputs\n", |
348 | | - "KEY_VAULT_URL=\"https://kv-xxxxx.vault.azure.net/\"\n", |
| 367 | + "KEY_VAULT_URL=\"<KEY-VAULT-URL>\"\n", |
349 | 368 | "\n", |
350 | 369 | "# Run the MCP agent\n", |
351 | | - "python3 ~/scripts/sample_agents_mcp.py $KEY_VAULT_URL\n", |
| 370 | + "python3 ~/sample_agents_mcp.py $KEY_VAULT_URL\n", |
352 | 371 | "```\n", |
353 | 372 | "\n" |
354 | 373 | ] |
|
367 | 386 | ], |
368 | 387 | "metadata": { |
369 | 388 | "kernelspec": { |
370 | | - "display_name": ".venv", |
| 389 | + "display_name": "base", |
371 | 390 | "language": "python", |
372 | 391 | "name": "python3" |
373 | 392 | }, |
|
381 | 400 | "name": "python", |
382 | 401 | "nbconvert_exporter": "python", |
383 | 402 | "pygments_lexer": "ipython3", |
384 | | - "version": "3.12.10" |
| 403 | + "version": "3.12.2" |
385 | 404 | } |
386 | 405 | }, |
387 | 406 | "nbformat": 4, |
|
0 commit comments