Install
$ agentstack add skill-aiappsgbb-awesome-gbb-foundry-mcp-aca ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
> 📦 This skill is for MCP server PRODUCERS (deploying servers to ACA). If you want to CONSUME an existing MCP server from a Foundry hosted agent, see [foundry-hosted-agents](../foundry-hosted-agents/SKILL.md) § MCP Tools or [foundry-toolbox](../foundry-toolbox/SKILL.md) § Learn MCP.
Foundry MCP ACA Deployment
> 🎯 Scope: PRODUCER-side only. This skill is for hosting MCP servers on > Azure Container Apps or Azure Functions, NOT for consuming a remote MCP from > a Foundry hosted agent (e.g. calling https://learn.microsoft.com/api/mcp or > https://api.github.com/mcp from your agent's container.py). > > - Producer (this skill): you're WRITING + DEPLOYING an MCP server. > Cosmos MCP, Playwright MCP, custom MCP for an internal API, etc. > - Consumer (different skill): you're WRITING a Foundry hosted agent that > CALLS a remote MCP server. Use [foundry-hosted-agents](../foundry-hosted-agents/SKILL.md) > § MCP Tools via FoundryChatClient + § MCP Tools — recommended pattern. > > Confusing the two costs hours. The consumer-side pattern is MCPStreamableHTTPTool > wired into Agent(tools=[…]); the producer-side is everything below.
> ⚠️ Azure Tenant Isolation (mandatory). Before any azd or az > operation, verify tenant isolation per > [azure-tenant-isolation](../azure-tenant-isolation/): set config dirs, > check token, assert subscription. See that skill's Agent preflight.
Deploy custom MCP servers as Azure Container Apps or Azure Functions for use with Foundry hosted agents. The hosted agent container connects to these MCP servers via HTTP at runtime using client.get_mcp_tool().
When to Use
- Deploying a Cosmos DB MCP server for agent data access
- Running Playwright/browser automation as a remote MCP server
- Creating a custom MCP server for an API or data store not covered by Foundry built-ins
- Deploying an MCP server as an Azure Function (consumption billing)
Architecture
┌────────────────────────┐ HTTPS ┌─────────────────────┐
│ Hosted Agent Container │ ─────────────► │ MCP ACA │
│ client.get_mcp_tool() │ │ (e.g. Cosmos MCP) │
│ Agent + ResponsesHost │ │ Port 8080 /mcp │
└────────────────────────┘ └─────────────────────┘
The hosted agent container:
- Loads
mcp-config.jsonat startup (orMCP_SERVER_URLenv var) - Creates
client.get_mcp_tool(name=..., url=..., approval_mode="never_require")per server - Passes tools to
Agent(tools=[...])alongside skill-loaded instructions
MCP Protocol Requirements
ALL 6 JSON-RPC methods must return HTTP 200 — even if the response body is empty {}. Failing to handle any of these causes FoundryChatClient.get_mcp_tool() to silently fail.
| Method | Purpose | Notes | |--------|---------|-------| | initialize | Protocol handshake | Must return server capabilities | | notifications/initialized | Client notification | Can return {} | | tools/list | Discover available tools | Must return tool definitions | | prompts/list | List prompts | Required by agent-framework (return empty list) | | resources/list | List resources | Required by agent-framework (return empty list) | | logging/setLevel | Set log level | Per MCP spec § Logging — camelCase setLevel (capital L). Lowercase setlevel returns -32601 Method not found from spec-compliant clients. |
Transport requirements:
- Foundry only accepts remote HTTP MCP endpoints (no stdio, no local)
- Use Streamable HTTP transport (HTTP POST with JSON-RPC at
/mcp) - Non-streaming tool call timeout: 100 seconds
- Private MCP (VNet) requires Standard Agent Setup
- Port 8080 is convention for ACA MCP servers
- Health endpoint at
/health(separate from MCP protocol)
Option A: Cosmos DB MCPToolKit (.NET)
A pre-built .NET Cosmos DB MCPToolKit image provides 10 tools out of the box:
| Tool | Type | Purpose | |------|------|---------| | list_databases | Read | List all Cosmos databases | | list_collections | Read | List containers in a database | | find_document_by_id | Read | Get single document by id | | text_search | Read | Text search across documents | | query_documents | Read | SQL query against a container | | get_approximate_schema | Read | Infer schema from sample docs | | get_recent_documents | Read | Get N most recent documents | | vector_search | Read | Semantic vector search | | upsert_document | Write | Create or update a document | | delete_document | Write | Delete a document by id |
Deployment
Deploy as a per-project ACA. Threadlight pilots are keyless-by-mandate — prefer managed identity over Cosmos keys.
| Variable | Required | Purpose | |----------|----------|---------| | COSMOS_ENDPOINT | ✅ | Cosmos DB account endpoint | | COSMOS_DATABASE | ✅ | Default database name | | AZURE_CLIENT_ID | ✅ (keyless) | UAMI client ID — the MCPToolKit's Cosmos SDK uses DefaultAzureCredential which reads this | | COSMOS_AUTH_KEY | ❌ avoid | Cosmos master key. Only for local dev; for ACA, disable account keys at the Cosmos resource (disableLocalAuth: true) and grant the UAMI Cosmos DB Built-in Data Contributor (data-plane RBAC, NOT control-plane Contributor) on the database scope | | DEV_BYPASS_AUTH | No | Set true only for local dev; never in prod |
> RBAC pin (verified May 2026). Cosmos DB SQL API data-plane access is > NOT granted by control-plane roles like Contributor or > DocumentDB Account Contributor. You MUST assign the data-plane role > Cosmos DB Built-in Data Contributor > (00000000-0000-0000-0000-000000000002) via az cosmosdb sql role > assignment create. This is the same gotcha called out in > threadlight-hitl-patterns — keep both wirings consistent.
> ⚠️ aiohttp dep is mandatory for the Python Cosmos MCP server. The Python Cosmos MCPToolKit uses azure-cosmos async client which > silently requires aiohttp as the HTTP transport. Without it the container starts > fine, tools/list returns the 11 tools, but every upsert_item / query_items > call fails server-side with what looks like a Cosmos error but is actually an > ImportError swallowed by FastMCP. Pin in src/mcp/requirements.txt: > > `` > fastmcp>=2.0.0, azure-cosmos>=4.15.0 # see kwarg callout below > azure-identity>=1.19.0 > mcp>=1.10.0 > aiohttp>=3.9.0 # REQUIRED — async HTTP transport for azure-cosmos > ``
> ⚠️ Pin fastmcp=2.0.0 pin is a re-deploy time bomb. > FastMCP 3.x changed the streamable-http mount path. If requirements.txt says > fastmcp>=2.0.0 (no upper bound), the next container rebuild will pull > fastmcp 3.x silently the moment it ships on PyPI. Symptoms — agent says > "case read failed" / "audit-log screening read failed" on every tool call, > bot/MCP logs show every single request as POST /mcp HTTP/1.1" 404 Not Found, > the MCP container itself is Healthy and Running. FastMCP itself prints > the warning at boot: > > `` > FastMCP 3.0 is coming! > Pin fastmcp `` > > If you skim past that and ship, every Cosmos tool call will 404. The KYC PoC > burned an hour on this when an unrelated azd deploy cosmos-mcp rebuild > jumped fastmcp 2.14.7 → 3.2.4 and broke point reads + queries simultaneously. > Always upper-bound: fastmcp>=2.0.0, client that imports fastmcp (e.g. an ACA Job that drives the MCP) — pin > client + server to the same major. > > 🛑 DO NOT bump fastmcp major version without re-running the demo scenarios — the streamable-http mount path changed between 2.x and 3.x. Every Cosmos tool call will fail silently if the path moves. DO test locally first:** pip install 'fastmcp>=3' && python -m pytest tests/ -k cosmos_mcp.
> ⚠️ enable_cross_partition_query was DROPPED in azure-cosmos>=4.15 async. > If your query_items tool implementation passes enable_cross_partition_query=True, > the kwarg leaks down to aiohttp.ClientSession._request() and raises > TypeError: ClientSession._request() got an unexpected keyword argument > 'enable_cross_partition_query'. FastMCP swallows the traceback and surfaces > only Error calling tool 'query_items' — the agent then says "case lookup > failed" on every related read while point reads (get_item) keep working. > > The new async signature is partition-key-aware by inference: > > ``python > # ❌ Old (works on azure-cosmos=4.15): > async for item in container.query_items( > query=q, parameters=p, enable_cross_partition_query=True, > ): > ... > > # ✅ New: omit partition_key for cross-partition; pass it for single-partition: > kwargs = {"query": q, "parameters": p} > if partition_key is not None: > kwargs["partition_key"] = partition_key > async for item in container.query_items(**kwargs): > ... > ` > > This is a **silent migration trap** — the SDK dependency floats forward, the > kwarg used to be valid, and the runtime error message blames the tool name > not the SDK call. Catches every Cosmos MCP that pinned azure-cosmos>=4.7 > instead of >=4.15`. > > 🛑 DO NOT rely on cross-partition queries for MCP tool calls without explicit user consent — partition-scoped queries are cheaper and more predictable. DO scope per-partition or accept the cost & latency impact. If you must cross-partition, document it as a tool contract in SPEC § 6 so the agent knows to prefer partition-scoped alternatives when available.
> ⚠️ azd deploy poisons every running agent's MCP session — must redeploy the agent too. > FastMCP's streamable-http maintains per-client session state in-memory on the MCP > container. When you redeploy the MCP server (azd deploy cosmos-mcp / > azd deploy / any new container revision), every session is wiped. > The Foundry hosted agent's MCP client caches the mcp-session-id from the previous > initialize handshake and keeps sending it with every tools/call — and does NOT > auto-detect "Session not found" + re-handshake. Result: every tool call returns > 404 silently, agent self-reports case read failed / audit-log query failed > on EVERY tool, MCP container is Healthy and Running, MCP logs show > POST /mcp HTTP/1.1" 404 Not Found without the preceding new transport > with session ID: ... log line that a fresh handshake would produce. External > probes to /mcp with proper Accept headers return 200 OK — the path is fine, > the SESSION is gone. > > Distinguishing this from the FastMCP 3.x mount-path 404: > > | Symptom | FastMCP 3.x mount-path | Stale session-id | > |---|---|---| > | MCP log line | POST /mcp HTTP/1.1" 404 (no transport log either way) | POST /mcp HTTP/1.1" 404 (no new transport with session ID log preceding) | > | External probe with Accept: application/json, text/event-stream | 404 Not Found (path moved) | 200 OK (path fine) | > | External probe with stale mcp-session-id header | 404 Not Found (path moved) | 404 {"error":{"code":-32600,"message":"Session not found"}} | > | Fix | Pin fastmcp > **Mandatory recovery sequence after redeploying any MCP server:** > > `bash > # After: azd deploy cosmos-mcp (or any MCP service) > # ALSO redeploy the agent so its in-memory MCP client cache is dropped: > azd deploy # creates a new agent version, fresh compute > > # And restart the bot ACA replica so its connection pool is dropped: > az containerapp revision restart \ > -g -n \ > --revision $(az containerapp revision list -g -n \ > --query "[?properties.active] | [0].name" -o tsv) > ` > > **Alternatively** — wait ~15 min idle and the refreshed-preview hosted agent > auto-deprovisions; the next user message will spin up fresh compute with a > fresh MCP session. But "wait 15 min" isn't a fix you can put in a runbook. > > **Where this should ideally be solved**: the agent runtime's MCP client should > catch JSON-RPC error code -32600 Session not found and re-initialize. Until > the platform handles this, treat MCP and agent as a **coupled deploy pair** — > you cannot redeploy one without the other on a running pilot. > > **🛑 DO NOT redeploy MCP server without re-importing the consuming agent version** — the cached session ID will become stale and every tool call will 404. **DO bump the agent version pin** (or call azd ai agent show` to refresh) immediately after each MCP redeploy. This is the most common production outage pattern on Foundry hosted agents.
Cosmos firewall + ACA egress (the trap that wastes 45 min on every fresh PoC)
The single biggest "first-deploy doesn't work" gotcha for ACA→Cosmos:
| Default | What happens | Fix | |---|---|---| | Cosmos publicNetworkAccess: Disabled (the Azure default) | All ACA→Cosmos traffic returns Forbidden — public access disabled | Set publicNetworkAccess: Enabled for pilots (production: use private endpoint) | | Cosmos networkAclBypass: None (default) AND ipRules: [] | All ACA→Cosmos traffic returns Forbidden — Request originated from IP through public internet. This is blocked | Either: (a) set networkAclBypass: AzureServices ⚠️ (see caveat) OR (b) add the ACA environment's egress IP to ipRules (proven path for pilots) | | networkAclBypass: AzureServices is set, ACA still blocked | Even with AzureServices bypass, ACA managed-environment egress can still be treated as "public internet" by Cosmos. The bypass DOES NOT cover ACA the way it covers Functions/Logic Apps. | Add the ACA egress IP to ipRules explicitly. Get it from the Cosmos Forbidden error message ("Request originated from IP X.X.X.X"), then az cosmosdb update -g -n --ip-range-filter "X.X.X.X". For prod, use a private endpoint instead. |
Bicep snippet (pilot-grade default — put this in infra/modules/cosmos-db.bicep):
@description('Pilot posture: enables public network with AzureServices bypass + room for explicit ACA egress IP. Set false for prod-grade (private endpoint).')
param pilotPosture bool = true
@description('Optional explicit IP allowlist (for ACA egress IPs). Pilots: leave empty initially, then re-deploy with the IP from the Cosmos Forbidden error.')
param ipAllowlist array = []
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' = {
// ...
properties: {
publicNetworkAccess: pilotPosture ? 'Enabled' : 'Disabled'
networkAclBypass: pilotPosture ? 'AzureServices' : 'None'
ipRules: [for ip in ipAllowlist: { ipAddressOrRange: ip }]
disableLocalAuth: true // keyless-by-mandate
// ...
}
}
Operator runbook for the "ACA→Cosmos Forbidden" failure (post-deploy):
# 1. Pull the egress IP from the cosmos-mcp ACA logs
az containerapp logs show -g -n ca--cosmos-mcp- --tail 200 \
| grep -i "Request originated from IP"
# Example output: "Request originated from IP 135.116.230.235 through public internet"
# 2. Add it to Cosmos firewall (REST PATCH; CLI does not expose --enable-public-network)
az rest --method PATCH \
--url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.DocumentDB/databaseAccounts/?api-version=2024-12-01-preview" \
--body '{"properties":{"publicNetworkAccess":"Enabled","ipRules":[{"ipAddressOrRange":"135.116.230.235"}]}}'
# 3. Wait 60-180s for Cosmos provisioningState to flip Updating -> Succeeded
az cosmosdb show -g -n --query "provisioningState" -o tsv
Or automate it as a postdeploy hook (recommended for fully unattended azd up). Reference implementation: [references/postdeploy_cosmos_firewall_egress.py](references/postdeploycosmosfirewall_egress.py) in this skill. The script:
- Reads
AZURE_RESOURCE_GROUP/AZURE_COSMOS_ACCOUNT_NAMEfrom azd env - Auto-discovers the cosmos-mcp ACA name (first ACA matching
*cosmos-mcp*) - Polls the ACA's recent console logs (up to 5 min) for the Cosmos Forbidden error patt
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: aiappsgbb
- Source: aiappsgbb/awesome-gbb
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.