foundry-mcp-aca
Skill8 starsChanged 12 days agoMITView on GitHub
Deploy custom MCP servers as Azure Container Apps or Azure Functions for use with Foundry hosted agents. Covers Cosmos DB MCPToolKit, Playwright MCP, custom MCP servers, protocol requirements, ACA configuration, and authentication + hardening (ACA built-in auth / Easy Auth, OAuth, managed identity). USE FOR: deploy MCP server, MCP on ACA, Cosmos MCP, Playwright MCP in Foundry, custom MCP server, Azure Functions MCP, MCP ACA deployment, remote MCP endpoint, MCP for hosted agent, connect hosted agent to MCP, secure MCP server, harden MCP server, MCP authentication, MCP OAuth, ACA Easy Auth for MCP. DO NOT USE FOR: deploying the hosted agent itself (use threadlight-deploy), long-running job-backed MCP orchestration or external ACA Job handoff (use foundry-mcp-aca-jobs), local MCP development (use mcp-config.json directly), general Azure deploy.
- Installs packages
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 7 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 55
Before preparing an image, record the selected ACA environment type, region, supported ingress/auth features, registry authorization mode …
- Distinctive: line 83
Follow the operation recovery contract for stateful tools and delivery adapters.
- Very specific: line 123
Use Streamable HTTP transport (HTTP POST with JSON-RPC at /mcp)
- Very specific: line 380
Do not mix legacy agent.yaml fields into a unified azure.yaml.
- Distinctive: line 423
No image was built or pushed.
- Distinctive: line 463
When backend systems are inaccessible (SAP, Oracle, corporate CRM, etc.), generate a
- Distinctive: line 474
When real system available: customer deploys a real MCP server, changes the URL
- Very specific: line 605
Use the standard azd workflow (see azure.yaml service binding in azd-patterns):
- Distinctive: line 613
When the real system becomes available:
- Very specific: line 615
Update mcp-config.json URL from mock endpoint to real endpoint
- Very specific: line 659
Put Entra in front of the container.
- Very specific: line 662
Read that header server-side (FastMCP's get_http_headers() — see secure_server.py).
- Distinctive: line 908
When a hosted agent runs a long instruction chain (10+ steps), even strong models occasionally call a "build the answer" tool before …
- Very specific: line 920
Every commit-style tool validates that its evidence_bundle (or equivalent input) contains the required fields.
- Very specific: line 990
Use a stable error enum (INSUFFICIENT_EVIDENCE, NOT_FOUND, STATE_CONFLICT, etc.) so the agent can pattern-match.
- Distinctive: line 1068
Every MCP server backed by a mocked system MUST ship two scripts so demos recover from failed runs in <30s:
- Very specific: line 1097
ONLY in dev/demo environments.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
📦 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 § MCP Tools or foundry-toolbox § Learn MCP. If the MCP server should hand work to an ACA Job, use foundry-mcp-aca-jobs instead.
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/mcporhttps://api.github.com/mcpfrom your agent'scontainer.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§ MCP Tools via FoundryChatClient + § MCP Tools — recommended pattern.Confusing the two costs hours. The consumer-side pattern is
MCPStreamableHTTPToolwired intoAgent(tools=[…]); the producer-side is everything below.
⚠️ Azure Tenant Isolation (mandatory). Before any
azdorazoperation, verify tenant isolation perazure-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)
Capability and identity preflight
Before preparing an image, record the selected ACA environment type, region,
supported ingress/auth features, registry authorization mode and exact
API/SDK cohort from this skill's upstream pin. An unsupported feature is
UNSUPPORTED_CAPABILITY; a missing owner choice is a blocker, not permission
to change the environment. Do not use the hosted agent's azd profile as proof
of compatibility for an ACA service.
Reuse the hosted capability/evidence vocabulary for the producer's read-only review. The producer has distinct subjects: deployer, ACA image-pull identity, inbound caller, server MI accessing the source, and independent result reader. For each, record the actual target, required data actions, effective roles/conditions and API version. An RG-wide elevated-role audit is not proof of these permissions.
Registry feasibility precedes build. Repository-only access requires an
ABAC registry, effective repository conditions and exclusion of broader pull
grants. Legacy AcrPull is registry-wide. Do not grant a broad role, change a
shared registry's mode or treat a successful deployer push as runtime pull.
The existing ACA module declares an identity; it does not prove that grant.
Ingress trust is deployment-specific. Easy Auth must enforce authentication on the actual route, replace caller-supplied principal headers, and leave no direct backend bypass. Socket peer, forwarded client IP and authenticated principal are different facts. Ignore arbitrary forwarded headers; trust only the explicitly verified proxy chain. No wildcard trusted-proxy shortcut. Private routing, authConfig support and effective config must be verified on the selected environment before depending on them.
Follow the operation recovery contract for stateful tools and delivery adapters. A tool error, generic404 or missing browser assertion is not permission to repeat a write. Capture the original operation identity/status before parsing and use the declared result reader.
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
Successful JSON-RPC requests return HTTP 200; accepted notifications return HTTP 202 (per MCP 2025-06-18 transport spec).
Authentication, transport and invalid-session failures retain their meaningful
HTTP status; never turn them into a success-shaped response.
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 | HTTP 202 Accepted (no body) |
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. Prefer keyless (managed identity) over Cosmos keys —
disable account keys at the Cosmos resource (disableLocalAuth: true).
| 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
ContributororDocumentDB Account Contributor. You MUST assign the data-plane roleCosmos DB Built-in Data Contributor(00000000-0000-0000-0000-000000000002) viaaz cosmosdb sql role assignment create. This is the same gotcha called out inthreadlight-hitl-patterns— keep both wirings consistent.
⚠️ aiohttp dep is mandatory for the Python Cosmos MCP server. The Python Cosmos MCPToolKit uses
azure-cosmosasync client which silently requiresaiohttpas the HTTP transport. Without it the container starts fine,tools/listreturns the 11 tools, but everyupsert_item/query_itemscall fails server-side with what looks like a Cosmos error but is actually an ImportError swallowed by FastMCP. Pin insrc/mcp/requirements.txt:fastmcp>=2.0.0,<3.0.0 # MUST upper-bound — see callout below 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<3.0.0— the unbounded>=2.0.0pin is a re-deploy time bomb. FastMCP 3.x changed the streamable-http mount path. Ifrequirements.txtsaysfastmcp>=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 asPOST /mcp HTTP/1.1" 404 Not Found, the MCP container itself isHealthyandRunning. FastMCP itself prints the warning at boot:FastMCP 3.0 is coming! Pin `fastmcp < 3` in production, then upgrade when you're ready.If you skim past that and ship, every Cosmos tool call will 404. A real deployment burned an hour on this when an unrelated
azd deployrebuild jumped fastmcp 2.14.7 → 3.2.4 and broke point reads + queries simultaneously. Always upper-bound:fastmcp>=2.0.0,<3.0.0. Same rule applies to any client that importsfastmcp(e.g. an ACA Job that drives the MCP) — pin client + server to the same major.🛑 DO NOT bump
fastmcpmajor 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_querywas DROPPED inazure-cosmos>=4.15async. If yourquery_itemstool implementation passesenable_cross_partition_query=True, the kwarg leaks down toaiohttp.ClientSession._request()and raisesTypeError: ClientSession._request() got an unexpected keyword argument 'enable_cross_partition_query'. FastMCP swallows the traceback and surfaces onlyError 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:
# ❌ Old (works on azure-cosmos<4.15, breaks on >=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.7instead 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.
Historical stale-session observation after MCP redeploy — verify the selected client. 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 <mock-mcp-service>/ any new container revision), every session is wiped. In the observed older client, the cachedmcp-session-idfrom the previous initialize handshake kept being sent withtools/callwithout a successful re-handshake. In that incident, tool calls returned 404 silently, agent self-reportscase read failed/audit-log query failedon EVERY tool, MCP container isHealthyandRunning, MCP logs showPOST /mcp HTTP/1.1" 404 Not Foundwithout the precedingnew transport with session ID: ...log line that a fresh handshake would produce. External probes to/mcpwith proper Accept headers return200 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(nonew transport with session IDlog preceding)External probe with Accept: application/json, text/event-stream404 Not Found(path moved)200 OK(path fine)External probe with stale mcp-session-idheader404 Not Found(path moved)404 {"error":{"code":-32600,"message":"Session not found"}}Discriminator before remediation Verify the actual mounted route and pinned package Verify the original session error and the client's reinitialization contract Historical recovery sequence, not an automatic repair:
# After: azd deploy cosmos-mcp (or any MCP service) # Only after identifying the stale session and obtaining explicit authorization: azd deploy <agent-service-name> # creates a new agent version, fresh compute # And restart the bot ACA replica so its connection pool is dropped: az containerapp revision restart \ -g <rg> -n <bot-aca-name> \ --revision $(az containerapp revision list -g <rg> -n <bot-aca-name> \ --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.
Client reinitialization support is version-specific. This incident does not establish a universal coupled-redeploy requirement or prove that a failed business call had no effect.
Before any recovery, distinguish a wrong route, auth failure and an actual stale session with a bounded handshake/read-only probe using the same client cohort.
azd ai agent showis a read, not a cache refresh. Do not automatically redeploy the agent or restart the bot, and never replay the failed business tool to diagnose its session. If reinitialization cannot be proven safe for the original operation, preserve UNKNOWN and return the specific blocked decision.
Cosmos firewall + ACA egress (the trap that wastes 45 min on every fresh deployment)
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 development (production: use private endpoint) |
Cosmos networkAclBypass: None (default) AND ipRules: [] | All ACA→Cosmos traffic returns Forbidden — Request originated from IP <egress-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 non-production environments) |
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 <rg> -n <acct> --ip-range-filter "X.X.X.X". For prod, use a private endpoint instead. |
Bicep snippet (development-grade default — put this in infra/modules/cosmos-db.bicep):
@description('Pilot posture (canonical — see azd-patterns): 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 <rg> -n ca-<process>-cosmos-mcp-<token> --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/<sub>/resourceGroups/<rg>/providers/Microsoft.DocumentDB/databaseAccounts/<acct>?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 <rg> -n <acct> --query "provisioningState" -o tsvOr automate it as a postdeploy hook (recommended for fully unattended azd up).
Reference implementation: references/postdeploy_cosmos_firewall_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 pattern
- Extracts the egress IP, idempotently patches Cosmos
ipRules - Polls Cosmos
provisioningStateuntilSucceeded
Wire it into azure.yaml:
hooks:
postdeploy:
shell: pwsh
run: |
cd infra/scripts && uv sync --frozen
uv run postdeploy.py
uv run postdeploy_cosmos_firewall_egress.pyThis closes the last manual step in the "1-shot azd up for Cosmos-using
processes" flow. Verified design against real deployment experience.
Agent Configuration
In the hosted agent's mcp-config.json:
{
"servers": {
"cosmos-tools": {
"type": "http",
"url": "${MCP_SERVER_URL}/mcp"
}
}
}Set MCP_SERVER_URL in the environment field of the selected
hosted profile.
Do not mix legacy agent.yaml fields into a unified azure.yaml.
Option B: Azure Functions (Consumption)
For lightweight, consumption-billed MCP servers:
# Scaffold from template
azd init --template remote-mcp-functions-python -e my-mcp-server
# Test locally
func start
# Deploy to Azure
azd up
# Endpoint: https://{app}.azurewebsites.net/runtime/webhooks/mcpAzure Functions use HTTP Streamable transport by default.
Option C: Custom ACA
Build a custom Docker image with your MCP tools and deploy as ACA.
CI fixture cleanup: shared infrastructure is never the teardown target.
The fixture uses
fixture_ownership.py to record
pre-create absence and a unique run tag, capture the exact app receipt, and
delete only that app after a fresh ownership/context check. The
failure guard runs cleanup on
intermediate shell failures without masking their exit status. Unknown creates
are reconciled by exact ID, not replayed. Image/deployment artifacts without
proven custody remain explicit inventory residuals; a prefix is not ownership
and a shared registry/repository must not be deleted to clean one image.
The cleanup fix has offline transport/shell coverage. A scoped manual check
on 2026-09-25 exercised the real helper with one new UUID-tagged MCR-placeholder
app through azd provision: the app was removed, and the separately proven
owned deployment-history record was deleted without touching shared resources.
No image was built or pushed. An unclassified post-delete 404 was not treated
as absence; a later typed ARM read confirmed removal. The helper now records
and re-observes that ambiguous post-delete response within its existing bound,
without reissuing DELETE. This is not proof of the full MCP azd up/auth flow;
the revised fixture still needs that live run before release. Pattern 25
separates functional CI results from cleanup reporting, not from safety.
Example: Playwright MCP on ACA
For browser automation in Foundry (hosted agent containers cannot run browsers locally):
FROM mcr.microsoft.com/playwright:v1.52.0-noble
RUN npm install -g @playwright/mcp
EXPOSE 8080
CMD ["npx", "@playwright/mcp", "--port", "8080"]Deploy as ACA with external ingress on port 8080.
Example: Custom Python MCP Server
MUST: Copy verbatim from
references/python/server.py. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the canonical FastMCP server with thetransport="streamable-http"+host="0.0.0.0"+port=os.environ["PORT"]trio (closes M2 + M3) plus the_helper()pattern for sharing logic between two@mcp.tool()decorated functions.
The three lines this template prevents from going wrong: transport="streamable-http" (not bare "http"), host="0.0.0.0" (not stdio default), and port=int(os.environ.get("PORT","8080")) (ACA injects PORT). Plus: never call one @mcp.tool()-decorated function from inside another — extract shared logic into a plain _helper().
Bicep: ACA for MCP Server
MUST: Copy verbatim from
references/bicep/mcp-aca.bicep. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the canonical ACA module with external ingress on:8080, UAMI for ACR pull (A2),transport: 'http'(not deprecated'auto'), and liveness + startup probes on/health(without them, cold-start tool calls 502 until the first scrape).minReplicas: 1avoids the cold-start hit on every demo.
VNet injection is intentionally out of scope here — see foundry-vnet-deploy SKILL for private topology. External ingress is required when the Foundry hosted agent (which runs in Foundry's infra, not your VNet) calls the MCP.
Option D: Mock MCP Server (for Prototyping / Demo)
When backend systems are inaccessible (SAP, Oracle, corporate CRM, etc.), generate a
FastMCP mock server backed by sample data from specs/sample-data/. The customer
sees real MCP tool calls with realistic responses — and later swaps the endpoint URL
for their real system.
How It Works
threadlight-designproducesspecs/sample-data/{entity}.json+ tool contracts in spec § 6- This skill generates a FastMCP server with tools matching those contracts
- Each tool reads from the sample data JSON files and returns matching records
- Deploy to ACA (or run locally for dev) — agent connects via
mcp-config.json - When real system available: customer deploys a real MCP server, changes the URL
Generate src/mcp/server.py
For each tool contract in specs/SPEC.md § 6 that is backed by a system marked mock
in § 5, generate a tool function:
"""Mock MCP server — auto-generated from SpecKit spec.
Replace with real system integration when available."""
import json
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP("mock-tools")
DATA_DIR = Path(__file__).parent / "data"
def _load(entity: str) -> list:
"""Load sample data for an entity."""
path = DATA_DIR / f"{entity}.json"
if not path.exists():
return []
data = json.loads(path.read_text())
return [r for r in data if not r.get("_meta")]
# === Auto-generated tools from spec § 6 ===
# Each tool matches a tool contract. Replace with real API calls when available.
@mcp.tool()
async def get_customer_profile(customer_id: str) -> str:
"""Look up customer profile. (Mock: reads from sample data)"""
records = _load("customers")
match = [r for r in records if r.get("customer_id") == customer_id]
if match:
return json.dumps(match[0], indent=2)
return json.dumps({"error": "not_found", "customer_id": customer_id})
# ... generate one @mcp.tool() per tool contract backed by a mocked system ...
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)Generate src/mcp/data/
Copy sample data from specs/sample-data/*.json into src/mcp/data/.
Generate src/mcp/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
COPY data/ data/
EXPOSE 8080
CMD ["python", "server.py"]Pin
fastmcp.fastmcphad a 1.x → 2.x API break (Client interface changed). Both server (src/mcp/) and any client that importsfastmcp(e.g. an ACA Job that drives the MCP server) MUST pin to the same major to avoid silent failures: container starts,tools/listworks, buttools/callreturns the wrong shape and the agent self-reportsfailed.
Generate src/mcp/requirements.txt
fastmcp>=2.0.0,<3.0.0
azure-cosmos>=4.7.0 # only if Cosmos MCP — see Option A
azure-identity>=1.19.0 # only if keyless to Azure
aiohttp>=3.9.0 # MANDATORY for Cosmos MCP — async transport for azure-cosmosMCP ACA also needs the fetch-container-image pattern (just like bot)
Verified in recent ACA port-mismatch retrospectives. When
infra/main.bicepdefaults the MCP image tomcr.microsoft.com/azuredocs/containerapps-helloworld:latest(the standard "first-deploy" placeholder), the ACA gets stuckInProgressforever because the placeholder serves port 80 but the MCP module pinstargetPort: 8080. Thefetch-container-imagepattern is documented for bot ACAs infoundry-teams-botSKILL.md but MUST be applied to MCP ACAs too. Same trap, same fix:
// infra/main.bicep — MCP ACA wiring
@description('Set true after the first azd deploy mcp; lets re-provision preserve the deployed image')
param mcpResourceExists bool = false
module fetchMcpImage 'mcp/fetch-container-image.bicep' = if (mcpResourceExists) {
name: 'fetch-mcp-image'
params: {
name: 'ca-${prefix}-mcp-${token}'
}
}
module mcp 'mcp/aca.bicep' = {
params: {
image: mcpResourceExists && !empty(fetchMcpImage.outputs.image)
? fetchMcpImage.outputs.image
: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
targetPortOverride: mcpResourceExists ? 8080 : 80 // KEY: 80 on first deploy (helloworld), 8080 thereafter
// ... rest of params
}
}The fetch-container-image.bicep shape is identical to the bot version
in foundry-teams-bot/templates/infra/bot/fetch-container-image.bicep —
copy it into infra/mcp/ and adjust the resource type.
threadlight-safe-check will catch the helloworld placeholder if you
forget; see its --phase post-deploy image-probe step.
Local Development
cd src/mcp
pip install -r requirements.txt
python server.py
# MCP endpoint: http://localhost:8080/mcpDeploy to ACA
Use the standard azd workflow (see azure.yaml service binding in azd-patterns):
azd up # provisions Bicep + builds image to ACR + patches ACASwap to Real System
When the real system becomes available:
- Deploy a real MCP server (see Options A–C above)
- Update
mcp-config.jsonURL from mock endpoint to real endpoint - Redeploy the agent (
azd deploy <service>) - The tool contracts stay the same — only the backing implementation changes
Securing your MCP server
For a delegated-only custom Entra API, use the separate foundry-mcp-auth candidate. Its strict scope policy and safe receipts do not change this skill's app-only defaults. Issuer/audience validation alone is authentication, not per-user business authorization. The candidate records the live-proven delegated paths and keeps remaining multi-user and lifecycle gates explicit.
The perimeter is yours to build. A public survey found only 8.5% of reachable MCP servers require OAuth — the rest are unauthenticated remote tool-execution endpoints, i.e. open RCE. A remote MCP server with no identity check is not "internal-only by obscurity"; it is exploitable. This section hardens the ACA deployment above with Entra-validated auth in front of the container, managed identity for the hops behind it, and audit + input defenses on the tools themselves.
This skill uses ACA built-in authentication ("Easy Auth for Container Apps") — not Azure App Service. The platform validates the caller's token before a request reaches your code. Reference: Authentication and authorization in Azure Container Apps.
Threat model → defense mapping
| Threat | Mitigating layer | Where it lives |
| Unauthenticated tool invocation (open RCE) | L1 ACA built-in auth + Return401 | references/bicep/mcp-aca-auth.bicep / az containerapp auth |
| Confused deputy — server replays the caller's token downstream | L2 managed identity; never forward the inbound token | references/python/secure_server.py |
| Over-broad RBAC → lateral movement | L2 one least-privilege role assignment | references/bicep/mcp-aca-auth.bicep |
| Secret exfiltration through a tool result | L3 return secret metadata, never values; Key Vault refs | references/python/secure_server.py |
| Path traversal / command injection in tool args | L3 allow-list inputs; reject .. / \ ; | ` $( | references/python/secure_server.py |
| Undetected abuse (invocation floods, probing) | L5 one audit event per call → scheduled-query alert | references/python/secure_server.py + foundry-observability |
Defense-in-depth: skipping L1 makes everything else moot; L2–L5 keep a breach from spreading.
Layer 1 — Identity perimeter: ACA built-in auth
Put Entra in front of the container. ACA validates the JWT before it reaches
your app and injects a trusted X-MS-CLIENT-PRINCIPAL header (base64 JSON the
client cannot forge). Anonymous calls get 401 — they never touch your tools.
Read that header server-side (FastMCP's get_http_headers() — see
secure_server.py); never accept the
principal as a tool argument, which the client controls and can forge.
One-time app registration (defines the audience clients request a token for):
TENANT_ID=$(az account show --query tenantId -o tsv)
APP_ID=$(az ad app create --display-name "mcp-server-auth" \
--sign-in-audience AzureADMyOrg --query appId -o tsv)
az ad sp create --id "$APP_ID"
# Expose api://<appId> as the resource callers request a token for:
az ad app update --id "$APP_ID" --identifier-uris "api://$APP_ID"Enable built-in auth + reject anonymous callers:
az containerapp auth microsoft update -n <app> -g <rg> \
--client-id "$APP_ID" \
--issuer "https://login.microsoftonline.com/$TENANT_ID/v2.0" \
--allowed-token-audiences "api://$APP_ID" --yes
az containerapp auth update -n <app> -g <rg> \
--unauthenticated-client-action Return401⚠️ Audience-scoped only. These two commands validate audience + issuer — an authentication check, not delegated scope or data authorization. Enforce the user's required
scppermissions separately. The app-only server-to-server model (this skill's primary consumer) needs two more adjustments: see the callout below, or use the Bicep reference, which encodes both.
Return401 is the API-server posture (reject), versus RedirectToLoginPage
(browser apps) or AllowAnonymous (pass everything through — never for a remote
MCP). The IaC-native equivalent — a Microsoft.App/containerApps/authConfigs
child resource, validation-only so it needs no client secret — is
references/bicep/mcp-aca-auth.bicep.
App-only callers need explicit allow-listing — a mismatch is
403, not401. A server-to-server / MI bearer token (this skill's primary consumer model) clears the anonymous check but is still denied403unless its client id is indefaultAuthorizationPolicy.allowedApplications. Two more app-only quirks the reference Bicep encodes (all three verified against a live ACA Easy Auth deployment):
- a v2 app-only token's
audis the bare client GUID, notapi://<appId>— list both forms inallowedAudiencesso delegated and app-only callers pass audience validation;- authConfig readback is desired configuration, not loaded-policy proof. Compare an authenticated/unauthenticated probe on the actual route with the selected config and revision. A stale response can have several causes. Do not automatically restart, PATCH again or recreate; one authorized reload is a discriminator only when the environment supports it, not a guaranteed fix. A restart error alone does not identify the root cause.
Caveat — no Dynamic Client Registration. Entra does not implement DCR, so interactive MCP clients must be pre-registered; ship a known client id rather than expecting the client to self-register.
These options replace the old dev-only table:
| Option | When | Posture |
Built-in auth + Return401 (recommended) | Any reachable deployment | Entra-validated bearer |
AllowAnonymous + a dev bypass flag | Local inner-loop only | No perimeter — never expose |
Layer 2 — Managed identity and the confused-deputy rule
The server authenticates to Azure with its own managed identity, never the
caller's token. DefaultAzureCredential resolves az login locally and the
user-assigned MI in ACA — no secrets in code:
MUST: downstream Azure access uses the server's MI. Copy the credential + secret-metadata pattern verbatim from
references/python/secure_server.py.
The confused-deputy rule: the inbound token authorizes the caller to your
server only. Do not replay it to Cosmos, Key Vault, or another API. Use the
server's MI, or the On-Behalf-Of flow if you genuinely must act as the user.
Grant that MI exactly one role (e.g. Key Vault Secrets User) — the role
assignment is in
references/bicep/mcp-aca-auth.bicep.
The secret_status reference currently calls get_secret and returns only
properties. That API/role can read the value; it is not a metadata-only
permission contract. If policy permits only metadata, stop before deployment
and select a reviewed metadata API/role instead. Do not grant Secrets User as
a silent fallback merely because the tool's returned payload omits the value.
Layer 3 — Secret hygiene and tool-input defense
Never return a secret value from a tool. Return metadata (name, version,
enabled) so the model can reason about a secret without exfiltrating it; the
value reaches the runtime via a Key Vault reference
(@Microsoft.KeyVault(SecretUri=...)), not through the model. The
secret_status tool in secure_server.py shows the pattern.
Reject unsafe tool inputs. Any argument that becomes a path, key, or shell
fragment is a traversal/injection vector. The safe_lookup tool in
secure_server.py rejects .., /, \, ;, |, `, and $( before
use — reject-by-default beats sanitizing.
Layer 4 — Network hardening
Easy Auth is an identity perimeter, not a network one. For regulated
workloads add, in order: private endpoints + VNET injection, then an APIM front
door running validate-jwt + rate-limit-by-key. The runtime-specific topology is
foundry-vnet-deploy — out of scope here.
External ingress is not "unauthenticated." The non-VNet default here uses external ingress with Layer 1 authentication. Do not generalize that default into a requirement for public access: app-level external ingress within an internal ACA environment can serve private VNet callers. Prove each Foundry runtime's actual route and DNS before using a private topology.
Layer 5 — Audit and monitoring
Emit one structured audit line per tool call (caller, tool, decision). ACA ships
stdout to Log Analytics automatically, so a
foundry-observability scheduled-query
alert can fire on invocation spikes or repeated denied events. The
audit_event helper in secure_server.py is the emitter; upgrade to Azure
Monitor OpenTelemetry spans via foundry-observability when you want
distributed tracing.
Connecting clients
| Client | How it authenticates | Notes |
| Server-to-server (Foundry agent, any service) | Its MI requests a token for api://<appId> and sends the bearer token | CI requires the standing MCP_AUTH_APP_CLIENT_ID audience and a distinct explicit caller ACL before deploying the MCP image; it checks anonymous 401, permitted 200, excluded-caller 403 and restored-policy 200 |
| Interactive — manual bearer (VS Code / Claude / Copilot) | Paste a token into the client's MCP config | Simplest interactive path |
| Interactive — OAuth discovery (advanced) | Server publishes PRM (RFC 9728); client follows WWW-Authenticate → user sign-in | Server's job on ACA — see below |
Manual bearer — a VS Code .vscode/mcp.json fragment:
{
"servers": {
"my-mcp": {
"type": "http",
"url": "https://<aca-fqdn>/mcp",
"headers": { "Authorization": "Bearer ${input:mcpToken}" }
}
}
}Get the token with
az account get-access-token --resource api://<appId> --query accessToken -o tsv.
OAuth discovery (advanced). Unlike Azure App Service (whose preview platform
feature can auto-serve Protected Resource Metadata), on ACA the MCP server
itself must publish PRM at /.well-known/oauth-protected-resource (RFC 9728)
and emit a WWW-Authenticate challenge so a spec-compliant client can discover
the authorization server and run the flow. FastMCP's auth provider implements
this; see the
MCP authorization spec.
This path requires an interactive browser sign-in, so it is documented but not
CI-tested.
Gotchas
🔴 DO NOT use bare
MCP.run()(defaults to stdio transport). On ACA, you MUST usetransport="streamable-http"and bind to0.0.0.0:8080.
| Issue | Cause | Fix |
| MCP tools not appearing in agent | Server returns 400/404 on protocol methods | Requests must return HTTP 200; notifications must return HTTP 202 |
logging/setLevel returns -32601 Method not found | Server didn't implement the optional logging capability OR used wrong casing | Either implement the method per MCP spec § Logging (camelCase setLevel) and declare capabilities.logging, or simply omit the capability — Foundry's MCP client tolerates servers that don't expose logging. |
| MCP connection timeout | ACA not started (cold start) | Runtime retries automatically; set minReplicas: 1 for always-on |
invalid_payload error | ${ENV_VAR} in mcp-config.json not resolved → empty URL | Only include MCP servers with deployed endpoints. Remove entries with unresolved env vars. |
| Tools listed but calls fail | Tool call timeout (>100s) | Optimize tool implementation or increase ACA resources |
FastMCP server starts but returns 000/timeout on port | MCP.run() with no args defaults to stdio transport (reads stdin, never binds HTTP port). ACA health probe fails, container marked unhealthy. | 🛑 DO NOT use bare MCP.run() on ACA — it defaults to stdio. DO use MCP.run(transport="streamable-http", host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))). Note: transport="streamable-http" (with the dash) matches the canonical examples elsewhere in this SKILL; the legacy bare http transport name is stale. |
TypeError: 'FunctionTool' object is not callable inside a tool | @MCP.tool wraps the function in a FunctionTool object. Calling it from Python (e.g., tool A calls tool B internally) raises TypeError. | Extract shared logic into a plain _helper() function. Both @MCP.tool functions call the helper. Never call one @MCP.tool-decorated function from inside another. |
| MCP ACA deployed but Foundry agent can't reach it (connection timeout) | az containerapp create --ingress internal — only resources in the same VNET can reach it. Foundry hosted agents run in Foundry's own infrastructure, not your VNET. | Use --ingress external for MCP ACA containers that Foundry agents call. Internal ingress only works for VNET-injected agents (private topology) where the agent subnet is peered/injected into the same VNET. See foundry-vnet-deploy. External ingress is safe when fronted by ACA built-in auth (see § Securing your MCP server → Layer 1); it is not the same as "unauthenticated." |
prompts/list not implemented | Server doesn't handle this method | Return {"prompts": []} — agent-framework requires it |
MCP 404 after session teardown | A late request may target the removed session; a 404 alone does not establish this cause | Retain the original session/request correlation and verify the completed work independently. Do not suppress unknown-session POST failures with a success-shaped 204 and do not replay business work to replace logs. |
Failure Modes & Recovery (Consumer Config / Deployment Coupling)
| Symptom | Root cause | DO NOT do | DO instead |
| Consumer config points to wrong URL (env var not resolved at deploy time) | ${MCP_SERVER_URL} expands to empty at agent-startup time, not deployment time | DO NOT use unguarded ${VAR} substitution in mcp-config.json — if the var is undefined, the agent skips the server silently | DO guard with validation: if not url or not url.startswith("http"): raise ValueError(f"Invalid MCP URL: {url}") in agent startup. Fail fast + audit-log. |
| Possible stale session after redeploy | Lost in-memory session is one candidate, not the meaning of every 404 | Do not replay the original tool or force a new agent version from a generic404 | Use a bounded read/handshake discriminator; retain original IDs and reconcile any effect before explicitly authorized recovery. A show command does not refresh runtime configuration. |
| Wrong mount path (404 on MCP calls) | FastMCP 3.x serves on /mcp/ with trailing slash; consumer config or curl tests use / without slash | DO NOT assume FastMCP mount path — it changed between 2.x and 3.x; don't infer from version. Test explicitly. | DO test with curl https://<aca-fqdn>/mcp/ -H "Accept: application/json, text/event-stream" (note trailing slash). Returns 200 OK if path is correct. For local: curl http://localhost:8080/mcp/. |
MCP call returns 401 after enabling built-in auth | Caller sent no token, or a token for the wrong resource | DO NOT switch to AllowAnonymous to "make it work" — that deletes the perimeter | Send Authorization: Bearer $(az account get-access-token --resource api://<appId> --query accessToken -o tsv); confirm the token aud equals api://<appId> |
401 even with a token attached | allowedAudiences / issuer mismatch, or a Graph token | DO NOT paste a Microsoft Graph token (--resource https://graph.microsoft.com) | Request the token for api://<appId>; verify --allowed-token-audiences includes exactly that value and issuer is https://login.microsoftonline.com/<tenant>/v2.0 |
Mock Data Generation — Delegate to threadlight-demo-data-factory
Important: The Mock MCP Server (Option D above) describes the MCP shell
— how to wrap mocked systems behind the protocol. It does NOT generate the
realistic sample data itself. Data generation is the responsibility of the
threadlight-demo-data-factory skill.
Division of labor
| Layer | Skill | Output |
| What data should look like (industry rules, golden cases, distributions) | threadlight-design (references/data-realism/<industry>.md) | Markdown rules per industry |
| How data is generated (Faker generators, seeding, reset, scripts) | threadlight-demo-data-factory | src/agent/data/seed.py + scripts |
| How tools wrap the data (MCP protocol, JSON-RPC, tool contracts) | foundry-mcp-aca (this skill) | src/mcp/server.py + Dockerfile + ACA Bicep |
Workflow when the spec marks a system as mock
SPEC § 5 says system X is "mock"
│
├── threadlight-design § 11d defines the data shape (volumes, golden cases, industry rules)
│
├── threadlight-demo-data-factory generates src/agent/data/*.json
│ (Faker + per-industry generator + seeded RNG + Cosmos seed/reset scripts)
│
└── foundry-mcp-aca (this skill) generates src/mcp/server.py
that READS src/agent/data/*.json AND/OR queries Cosmos (seeded by the factory)
and exposes the spec § 6 tool contracts via MCPThe src/mcp/data/ directory described in Option D §
"Generate src/mcp/data/" is populated by threadlight-demo-data-factory,
not by this skill. The script copying step in Option D should be replaced
with a call to the factory:
# Old (manual): cp specs/sample-data/*.json src/mcp/data/
# New (skill-driven):
uv run scripts/seed_data.py --to src/mcp/data/ # threadlight-demo-data-factoryValidate-or-reject (the canonical pattern for stateful tools)
Highest-leverage pattern in the toolchain. Observed in real deployments: lifted recommendation quality from "junk packet with
confidence: 0" to "well-citedconfidence: 0.93packet" with a single server-side change. Apply this to every MCP tool that commits a decision, persists state, or returns a high-stakes artifact (recommendation, approval, payment, allocation, contract).
The failure mode this fixes
When a hosted agent runs a long instruction chain (10+ steps), even strong models occasionally call a "build the answer" tool before calling the evidence-gathering tools. With a permissive server, this returns a hollow object — no transactions, no merchant lookup, no rule citations — and the agent confidently emits it as the final answer.
The most reliable fix is NOT to add more text to the agent's instructions ("ALWAYS call X before Y"). It's to make the server physically incapable of producing a hollow answer.
The pattern
Every commit-style tool validates that its evidence_bundle (or
equivalent input) contains the required fields. If anything is missing,
return a structured error with a next_steps array that names exactly
which tools to call. The agent reads this and self-corrects on the
next iteration.
# src/mcp/server.py — applies to ANY commit-style tool
@mcp.tool()
async def build_recommendation(
case_id: str,
evidence_bundle: dict, # the agent assembles this from prior tool calls
) -> str:
"""Build the final recommendation packet. REQUIRES complete evidence."""
REQUIRED = {
"transactions": "get_transaction_history",
"merchant_profile": "get_merchant_profile",
"prior_cases": "search_prior_cases",
"rule_citations": "lookup_reg_rule + lookup_network_rule",
}
missing = []
for field, source_tool in REQUIRED.items():
v = evidence_bundle.get(field)
if not v or (isinstance(v, list) and len(v) == 0):
missing.append({"field": field, "call_tool": source_tool})
if missing:
return json.dumps({
"error": "INSUFFICIENT_EVIDENCE",
"case_id": case_id,
"missing": missing,
"next_steps": [
f"Call `{m['call_tool']}` to populate evidence_bundle.{m['field']}"
for m in missing
],
"guidance": (
"Do not retry build_recommendation until every "
"missing field is populated."
),
}, indent=2)
# ... real recommendation construction here ...
return json.dumps(packet, indent=2)What good looks like (acceptance criteria)
| Test | Expected |
Call commit-tool with empty evidence_bundle | Returns INSUFFICIENT_EVIDENCE with full missing + next_steps (HTTP 200, error in payload — never raise) |
| Call commit-tool with partial evidence | Returns INSUFFICIENT_EVIDENCE listing only the missing fields |
| Call commit-tool with complete evidence | Returns the real packet |
| Smoke test on the agent end-to-end | Agent never emits a hollow packet — when it tries, it self-corrects within 1-2 extra tool calls |
Why structured (error + missing + next_steps), not free-form
A free-form "please call get_transaction_history first" works some
of the time. The structured shape is the difference between "works
high and low reproducibility in recent strict-smoke runs. Models
parse structured payloads more reliably than English instructions in
tool outputs.
Severity & error semantics
- Domain validation failures use a structured tool error. Do not suppress authentication/transport failures or infer that every client retries them. Measure dispatch behavior for the pinned client. An uncertain write is not safely retryable merely because a domain error can be rendered as JSON.
- Use a stable
errorenum (INSUFFICIENT_EVIDENCE,NOT_FOUND,STATE_CONFLICT, etc.) so the agent can pattern-match. next_stepsMUST name the exact tool name the agent has access to — do not write "go look it up", write"Call \get_transaction_history` with customer_id=…"`.
When NOT to apply this pattern
- Pure-read tools (
get_transaction_history,lookup_*) — no state, no commit, no need to gate. - Trivial tools that take only the inputs they validate (e.g.,
compute_business_days(start, end)) — there's nothing to gate against. - Tools whose entire purpose IS to fail-fast on missing args (the validation IS the answer).
Cross-reference
This pattern combines with threadlight-design SPEC § 6 tool contracts:
each commit-style tool's contract should explicitly enumerate its
required evidence_bundle fields and the error enums it can return.
The MCP server then mirrors those contracts as the validate-or-reject
guard above.
Streaming & Webhook Primitives (for live-event MCP servers)
Some processes (Order Fallout, Network Fault, Supplier Risk news ingestion) need MCP tools that return streaming live data rather than read-once records. Two patterns:
Pattern 1 — tools/call with progressive notifications
For long-running tools that should stream partial results (e.g., "scanning 1247 orders for fallout matches"):
@mcp.tool()
async def scan_fallout_orders(ctx: Context) -> dict:
total = 1247
for i, order in enumerate(orders):
await ctx.report_progress(i, total, f"Scanning {order['order_id']}")
if matches_fallout_pattern(order):
yield order # streaming chunk
return {"scanned": total, "matched": match_count}The agent receives progress notifications via JSON-RPC and can render
them in the workspace UI.
Pattern 2 — webhook-fed MCP server
For event-driven sources (Service Bus, Event Grid), pair this skill with
threadlight-event-triggers:
External event source MCP server (this skill)
│ ▲
▼ │
[event-triggers receiver] ─writes to─► Cosmos / Storage
│
└── normalizes payload, dedups, writes to backing storeThe receiver scaffold (from threadlight-event-triggers) ingests and
normalizes; this skill's MCP server reads from the backing store and exposes
it through tool contracts. Do not poll external APIs from inside the MCP
server — it makes the agent slow and breaks the 100-second tool timeout.
Reset & Replay Scripts
Reset applies only to explicitly approved synthetic run-owned data. It is not reconciliation of a lost response, and must never erase unresolved effect evidence or reset a real business system to make a test pass.
Every MCP server backed by a mocked system MUST ship two scripts so demos recover from failed runs in <30s:
scripts/seed_mcp.py
Idempotent. Loads src/agent/data/*.json (generated by
threadlight-demo-data-factory) into the MCP backing store (in-memory cache
or Cosmos seeding). Safe to call repeatedly.
scripts/reset_mcp.py
Wipes any state the agent has mutated (case statuses moved, decisions recorded) and re-seeds. The agent does NOT need to be restarted — the MCP server re-reads on next tool call.
# scripts/reset_mcp.py — typical shape
async def reset():
await wipe_mutable_state() # agent-driven changes
await seed_baseline() # factory-generated data
await mark_reset(timestamp=now()) # surface in workspace UI
if __name__ == "__main__":
asyncio.run(reset())
print("MCP reset complete.")The reset endpoint MAY also be exposed as an HTTP route on the MCP server
(POST /admin/reset) for one-click reset from the workspace UI — but
ONLY in dev/demo environments. Gate behind DEV_BYPASS_AUTH=true.
Input contract / Output artifacts
| Reads | From | ||
| SPEC.md § 5 Integrations | threadlight-design — flags each system as `real | mock | hybrid` |
| SPEC.md § 5b External Systems & Mocks | threadlight-design — per-system MCP shape and tool list | ||
| SPEC.md § 6 Tool Contracts | threadlight-design — exact tool signatures the MCP server must expose | ||
src/agent/data/*.json | threadlight-demo-data-factory — pre-generated sample data | ||
| SPEC.md § 10b Triggers | threadlight-design — tells you whether to also wire an event receiver (delegated to threadlight-event-triggers) |
| Produces | At |
src/mcp/server.py | One MCP server per mocked system |
src/mcp/Dockerfile | Build artifact for ACA |
src/mcp/requirements.txt or pyproject.toml | Python deps |
infra/modules/aca-mcp.bicep | ACA module — composed by threadlight-deploy (delegates to azd-patterns Bicep library) |
scripts/seed_mcp.py | Idempotent reseed |
scripts/reset_mcp.py | Demo recovery (<30s) |
mcp-config.json entry | Wired into src/agent/mcp-config.json so the hosted agent can discover the MCP server |
See Also
| Skill | Use When |
| threadlight-design | Generates SPEC.md § 5 / § 5b / § 6 — the input contract |
| threadlight-demo-data-factory | Generates the JSON files this MCP server reads |
| threadlight-event-triggers | Pairs with this skill for webhook-fed MCP servers (event source → receiver → backing store → MCP read) |
| threadlight-deploy | Composes the MCP ACA into the overall agent project (Phase 6 module composer) |
| azd-patterns | Bicep module library — aca-mcp.bicep is one of the composable modules |
| foundry-hosted-agents | The hosted agent that consumes the MCP server's tools |
| foundry-mcp-aca-jobs | ACA Job-backed companion for long-running MCP Tasks and external job orchestration |
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/llms.txtllms.txt
- agent-framework-harnessSkill
- auto-demo-producerSkill
- azd-patternsSkill
- azure-backup-readinessSkill
- azure-monitor-alert-baselineSkill
- azure-resource-diagnosticsSkill
- azure-sre-agentSkill
- azure-tenant-isolationSkill
- citadel-hub-deploySkill
- citadel-spoke-onboardingSkill
- copilot-doctorSkill
- foundry-agentopsSkill
- foundry-agtSkill
- foundry-caphost-lifecycleSkill
- foundry-cost-monitoringSkill
- foundry-cross-resourceSkill
- foundry-doc-vision-speechSkill
- foundry-evalsSkill
- foundry-hosted-agentsSkill
- foundry-iqSkill
- foundry-mcp-aca-jobsSkill
- foundry-mcp-authSkill
- foundry-network-runbookSkill
- foundry-observabilitySkill
- foundry-prompt-agentsSkill
- foundry-rbac-auditSkill
- foundry-routinesSkill
- foundry-skill-catalogSkill
- foundry-teams-botSkill
- foundry-toolboxSkill
- foundry-vnet-deploySkill
- foundry-voice-agentsSkill
- foundry-voice-liveSkill
- gbb-humanizerSkill
- gbb-pptxSkill
- ghcp-cli-configSkill
- ghcp-hosted-agentsSkill
- html-deck-to-pptxSkill
- html-presentationSkill
- ip-catalogSkill
- paygo-ptu-cost-analyzerSkill
- progress-guardSkill
- token-economicsSkill
- web-experience-designSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

