Skip to content

Commit aaed63a

Browse files
committed
Update template 19 testing guide and add MCP HTTP server
Updates based on actual testing: - Fixed SDK usage: use AzureAISearchTool class, not dict format - Added note about portal limitation with network injection - Added correct API version (2025-05-15-preview) for REST API - Added MCP HTTP server implementation (Streamable HTTP transport) - Updated MCP deployment instructions with proper HTTP-based server - Added connection name lookup command - Fixed test script to use AzureAISearchTool class MCP Server: - Added mcp-http-server/server.py - Flask-based MCP server - Added mcp-http-server/Dockerfile for containerization - Implements JSON-RPC over HTTP as required by Azure AI Agents
1 parent a9c504b commit aaed63a

File tree

4 files changed

+314
-44
lines changed

4 files changed

+314
-44
lines changed

infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/TESTING-GUIDE.md

Lines changed: 135 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -138,34 +138,66 @@ az search service update -g $RESOURCE_GROUP -n $AI_SEARCH_NAME \
138138

139139
## Step 4: Deploy MCP Server (Optional)
140140

141-
Deploy an MCP server to the private VNet:
141+
Deploy an HTTP-based MCP server to the private VNet.
142+
143+
> **Important**: Azure AI Agents require MCP servers that implement the **Streamable HTTP transport** (JSON-RPC over HTTP). Standard stdio-based MCP servers (like `mcp/hello-world`) will NOT work.
144+
145+
### 4.1 Create Container Apps Environment
142146

143147
```bash
144148
# Get MCP subnet resource ID
145149
MCP_SUBNET_ID=$(az network vnet subnet show -g $RESOURCE_GROUP --vnet-name $VNET_NAME -n "mcp-subnet" --query "id" -o tsv)
146150

147-
# Create Container Apps environment
151+
# Create Container Apps environment (internal only)
148152
az containerapp env create \
149153
--resource-group $RESOURCE_GROUP \
150154
--name "mcp-env" \
151155
--location $LOCATION \
152156
--infrastructure-subnet-resource-id $MCP_SUBNET_ID \
153157
--internal-only true
158+
```
159+
160+
### 4.2 Deploy HTTP-based MCP Server
154161

155-
# Deploy test MCP server
162+
An HTTP-based MCP server is provided in `mcp-http-server/`. Deploy it:
163+
164+
```bash
165+
# Build and deploy (requires ACR with managed identity access)
166+
cd mcp-http-server
167+
168+
# Create ACR and build
169+
ACR_NAME="mcpacr$(date +%s | tail -c 5)"
170+
az acr create --name $ACR_NAME --resource-group $RESOURCE_GROUP --sku Basic --location $LOCATION
171+
az acr build --registry $ACR_NAME --image mcp-hello-http:v1 --file Dockerfile .
172+
173+
# Create user-assigned identity with AcrPull role
174+
az identity create --name mcp-identity --resource-group $RESOURCE_GROUP --location $LOCATION
175+
IDENTITY_ID=$(az identity show --name mcp-identity -g $RESOURCE_GROUP --query "id" -o tsv)
176+
IDENTITY_PRINCIPAL=$(az identity show --name mcp-identity -g $RESOURCE_GROUP --query "principalId" -o tsv)
177+
ACR_ID=$(az acr show --name $ACR_NAME --query "id" -o tsv)
178+
az role assignment create --assignee $IDENTITY_PRINCIPAL --role AcrPull --scope $ACR_ID
179+
180+
# Deploy container app
156181
az containerapp create \
157182
--resource-group $RESOURCE_GROUP \
158-
--name "mcp-test-server" \
183+
--name "mcp-http-server" \
159184
--environment "mcp-env" \
160-
--image "mcr.microsoft.com/k8se/quickstart:latest" \
185+
--image "${ACR_NAME}.azurecr.io/mcp-hello-http:v1" \
161186
--target-port 80 \
162-
--ingress external \
163-
--min-replicas 1
187+
--ingress internal \
188+
--min-replicas 1 \
189+
--user-assigned $IDENTITY_ID \
190+
--registry-server "${ACR_NAME}.azurecr.io" \
191+
--registry-identity $IDENTITY_ID
192+
```
193+
194+
### 4.3 Configure Private DNS
164195

165-
# Get the FQDN and static IP
166-
MCP_FQDN=$(az containerapp show -g $RESOURCE_GROUP -n "mcp-test-server" --query "properties.configuration.ingress.fqdn" -o tsv)
196+
```bash
197+
# Get environment info
167198
MCP_STATIC_IP=$(az containerapp env show -g $RESOURCE_GROUP -n "mcp-env" --query "properties.staticIp" -o tsv)
168199
DEFAULT_DOMAIN=$(az containerapp env show -g $RESOURCE_GROUP -n "mcp-env" --query "properties.defaultDomain" -o tsv)
200+
MCP_FQDN=$(az containerapp show -g $RESOURCE_GROUP -n "mcp-http-server" --query "properties.configuration.ingress.fqdn" -o tsv)
169201

170202
echo "MCP FQDN: $MCP_FQDN"
171203
echo "Static IP: $MCP_STATIC_IP"
@@ -183,32 +215,57 @@ az network private-dns link vnet create \
183215
--registration-enabled false
184216

185217
# Add A records
186-
az network private-dns record-set a add-record \
187-
-g $RESOURCE_GROUP \
188-
-z $DEFAULT_DOMAIN \
189-
-n "mcp-test-server" \
190-
-a $MCP_STATIC_IP
218+
az network private-dns record-set a add-record -g $RESOURCE_GROUP -z $DEFAULT_DOMAIN -n "mcp-http-server" -a $MCP_STATIC_IP
219+
az network private-dns record-set a add-record -g $RESOURCE_GROUP -z $DEFAULT_DOMAIN -n "*" -a $MCP_STATIC_IP
220+
```
191221

192-
az network private-dns record-set a add-record \
193-
-g $RESOURCE_GROUP \
194-
-z $DEFAULT_DOMAIN \
195-
-n "*" \
196-
-a $MCP_STATIC_IP
222+
### 4.4 Test MCP with REST API
223+
224+
```python
225+
import requests
226+
from azure.identity import DefaultAzureCredential
227+
import time
228+
229+
credential = DefaultAzureCredential()
230+
token = credential.get_token("https://ai.azure.com/.default")
231+
232+
endpoint = "https://<ai-services>.services.ai.azure.com/api/projects/<project>"
233+
api_version = "2025-05-15-preview"
234+
mcp_url = "https://mcp-http-server.<default-domain>"
235+
236+
headers = {"Authorization": f"Bearer {token.token}", "Content-Type": "application/json"}
237+
238+
# Create agent with MCP tool
239+
agent_payload = {
240+
"model": "gpt-4o-mini",
241+
"name": "mcp-test-agent",
242+
"instructions": "Use the hello tool to greet users.",
243+
"tools": [{"type": "mcp", "server_label": "helloworld", "server_url": mcp_url}]
244+
}
245+
resp = requests.post(f"{endpoint}/assistants?api-version={api_version}", headers=headers, json=agent_payload)
246+
agent = resp.json()
247+
print(f"Agent: {agent['id']}")
197248
```
198249

199250
---
200251

201252
## Step 5: Test via Portal
202253

203-
Since the AI Services account has public access enabled, you can test directly in the portal.
254+
> **Note**: Portal testing may be blocked even with public access enabled if your deployment uses network injection (`networkInjections` property). In this case, use SDK testing (Step 6) instead.
204255
205-
### 5.1 Access the Portal
256+
### 5.1 Check if Portal Works
206257

207258
1. Navigate to [Azure AI Foundry portal](https://ai.azure.com)
208259
2. Sign in with your Azure credentials
209-
3. Select your project (created by the deployment)
260+
3. Toggle **"New Foundry"** ON (top right)
261+
4. Select your project
262+
263+
If you see this error:
264+
> "Your current setup uses a project, resource, region, custom domain, or disabled public network access that isn't supported in the new Foundry experience yet."
210265
211-
### 5.2 Create an Agent with AI Search Tool
266+
This is expected if network injection is configured. Use SDK testing instead.
267+
268+
### 5.2 Create an Agent with AI Search Tool (if portal works)
212269

213270
1. Go to **Agents** in the left menu
214271
2. Click **+ New agent**
@@ -250,43 +307,51 @@ For automated testing or CI/CD pipelines, use the SDK:
250307
### 6.1 Install Dependencies
251308

252309
```bash
253-
pip install azure-ai-projects azure-identity
310+
pip install azure-ai-projects azure-ai-agents azure-identity
254311
```
255312

256313
### 6.2 Run Test Script
257314

315+
Use the included `test_agents_v2.py` script or the following code:
316+
258317
```python
259318
#!/usr/bin/env python3
260319
"""Test agent with AI Search tool on private endpoint."""
261320

262321
import os
322+
import time
263323
from azure.ai.projects import AIProjectClient
324+
from azure.ai.agents.models import AzureAISearchTool
264325
from azure.identity import DefaultAzureCredential
265326

266-
# Configuration
327+
# Configuration - use project-scoped endpoint
267328
PROJECT_ENDPOINT = os.environ.get(
268329
"PROJECT_ENDPOINT",
269330
"https://<ai-services-name>.services.ai.azure.com/api/projects/<project-name>"
270331
)
332+
AI_SEARCH_CONNECTION = os.environ.get("AI_SEARCH_CONNECTION", "<connection-name>")
333+
AI_SEARCH_INDEX = os.environ.get("AI_SEARCH_INDEX", "test-index")
271334

272335
def main():
273336
client = AIProjectClient(
274337
credential=DefaultAzureCredential(),
275338
endpoint=PROJECT_ENDPOINT,
276339
)
340+
print(f"Connected to: {PROJECT_ENDPOINT}")
341+
342+
# Create AI Search tool using the SDK class (NOT dict format)
343+
search_tool = AzureAISearchTool(
344+
index_connection_id=AI_SEARCH_CONNECTION,
345+
index_name=AI_SEARCH_INDEX
346+
)
277347

278348
# Create agent with AI Search tool
279349
agent = client.agents.create_agent(
280350
model="gpt-4o-mini",
281351
name="sdk-search-agent",
282352
instructions="Search for information when asked.",
283-
tools=[{
284-
"type": "azure_ai_search",
285-
"azure_ai_search": {
286-
"index_connection_id": "<connection-name>",
287-
"index_name": "test-index"
288-
}
289-
}]
353+
tools=search_tool.definitions,
354+
tool_resources=search_tool.resources
290355
)
291356
print(f"Created agent: {agent.id}")
292357

@@ -299,25 +364,58 @@ def main():
299364
)
300365

301366
run = client.agents.runs.create(thread_id=thread.id, agent_id=agent.id)
302-
print(f"Run status: {run.status}")
367+
print(f"Started run: {run.id}")
368+
369+
# Wait for completion
370+
while run.status in ["queued", "in_progress"]:
371+
time.sleep(2)
372+
run = client.agents.runs.get(thread_id=thread.id, run_id=run.id)
373+
print(f"Status: {run.status}")
374+
375+
if run.status == "completed":
376+
messages = client.agents.messages.list(thread_id=thread.id)
377+
for msg in messages:
378+
if msg.role == "assistant":
379+
for content in msg.content:
380+
if hasattr(content, 'text'):
381+
print(f"Response: {content.text.value}")
382+
break
383+
print("✓ Test passed!")
384+
else:
385+
print(f"✗ Run failed: {run.status}")
303386

304387
# Cleanup
305388
client.agents.delete_agent(agent.id)
306-
print("Test complete!")
389+
print("Agent cleaned up")
307390

308391
if __name__ == "__main__":
309392
main()
310393
```
311394

395+
### 6.3 Find Your Connection Name
396+
397+
```bash
398+
# List connections in your project
399+
az rest --method GET \
400+
--url "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<ai-services>/projects/<project>/connections?api-version=2025-06-01" \
401+
--query "value[?properties.category=='CognitiveSearch'].name" -o tsv
402+
```
403+
312404
---
313405

314406
## Troubleshooting
315407

316408
### Portal Shows "New Foundry Not Supported"
317409

318-
This error occurs with **template 15** (fully private), not this template. If you see this error:
319-
- Verify you deployed template 19 (hybrid), not template 15
320-
- Check `publicNetworkAccess` is `Enabled` on the AI Services account
410+
This error can occur even with public access enabled if **network injection** is configured:
411+
412+
```bash
413+
# Check for network injection
414+
az cognitiveservices account show -g $RESOURCE_GROUP -n $AI_SERVICES_NAME \
415+
--query "properties.networkInjections"
416+
```
417+
418+
If you see `networkInjections` with a subnet configured, the portal's "New Foundry" experience won't work. **Use SDK testing instead** - it works perfectly with network injection.
321419

322420
### Agent Can't Access AI Search
323421

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
RUN pip install --no-cache-dir flask gunicorn
6+
7+
COPY server.py .
8+
9+
EXPOSE 80
10+
11+
CMD ["gunicorn", "--bind", "0.0.0.0:80", "--workers", "2", "server:app"]

0 commit comments

Comments
 (0)