Skip to content
Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

  • Home
  • Blog
  • About
  • Home
  • Blog
  • About
Close

Search

  • Home
  • Blog
  • About
Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

  • Home
  • Blog
  • About
  • Home
  • Blog
  • About
Close

Search

  • Home
  • Blog
  • About
A Foundry agent sends an Entra token to an MCP server on Azure Container Apps which answers 200 OK with the app role and 403 Forbidden without it
Implementation & OperationsMicrosoft FoundryUncategorized

Hosting Your Own MCP Server for Microsoft Foundry on Azure Container Apps

26.10.2026 12 Min Read
1

From a local whoami demo to a real remote MCP tool that Foundry calls with a Microsoft Entra token

Last verified against Microsoft Learn on 25 October 2026. Foundry changes quickly, so check the linked docs before you rely on a detail.

  • Foundry only calls remote MCP servers: A server on your laptop is invisible to Agent Service. Azure Container Apps is one of two documented hosts. The other is Azure Functions.
  • A valid token is not enough: With my own app registration Foundry got a valid token even though nobody had assigned an app role. Only a role check on the server closes that gap.
  • Entra authentication shows a workload and no person: The whoami answer proves it for the project managed identity and for the agent identity. If the tool must know the user you need OAuth identity passthrough.

The problem in plain language

In my last post I built a small MCP server called whoami. It validates a real Microsoft Entra token and tells you who is calling. It ran on my laptop. That is fine for a test but useless for Foundry. The docs are clear: “The Agent Service runtime only accepts a remote MCP server endpoint.”

So this post closes the gap. We put the same server on the internet, protect it with our own Entra app registration and let a Foundry agent call it. Then we read the whoami answer and see what the server really gets. On the way a few things went differently than the docs suggest. I wrote them down as they happened.

Sequence diagram with Microsoft Entra, Foundry Agent Service and an MCP server on Container Apps in four numbered steps
The agent asks Entra for a token and calls the server with it. The server checks signature, issuer, audience and app role

Where can the server live?

The docs name two hosts for a remote MCP server (Connect to MCP servers).

ConsiderationAzure Container AppsAzure Functions
TransportHTTP POST and GET endpoints requiredHTTP streamable required
AuthenticationCustom implementation requiredBuilt in authentication or custom code. A key is required by default
LanguageAny language in Linux containersPython, Node.js, TypeScript, Java and .NET
ContainerLinux amd64 only. No privileged containersContainers not supported
StateStateless onlyStateless only

I use Container Apps for two reasons. My demo is a Python container already. And the server validates the token itself, which is exactly the part I want to show.

Key insight: Both hosts are stateless only. That matches the demo, which starts with stateless_http=True and json_response=True. In the logs you can see it: every call is an independent POST /mcp. Do not keep session data in memory.

Step 1: Create the app registration for your API

Agent Service blocks tokens for Microsoft audiences from going to custom MCP servers. The error reads Cannot pass Microsoft token to untrusted MCP endpoint. The docs say your server “must be registered with an audience that you control”. So we create our own API with an app role that can be assigned to applications.

[{
  "allowedMemberTypes": ["Application"],
  "description": "Allows a workload such as a Foundry project identity to call the whoami MCP tool.",
  "displayName": "Call whoami MCP tool",
  "isEnabled": true,
  "id": "<new guid>",
  "value": "Mcp.Tools.ReadWrite.All"
}]

I saved this as app-roles.json and used it for the app registration. The role value is what later shows up in the roles claim (Add app roles).

az ad app create --display-name <api-name> --sign-in-audience AzureADMyOrg --app-roles @app-roles.json
az ad app update --id <app-id> --identifier-uris "api://<app-id>"
az ad sp create --id <app-id>

The Application ID URI api://<app-id> is the audience. I left the token version at its default (null). The tokens Foundry received were version 1 tokens: the issuer is sts.windows.net and the audience is the Application ID URI. If the connection asks for the client ID alone as audience the token carries the client ID as aud. That is why my server accepts both values in AUDIENCES.

Step 2: Deploy to Azure Container Apps

The container from the demo repo needs these environment variables.

VariableValue
TENANT_IDYour tenant
AUDIENCESapi://<app-id> and the client ID
PUBLIC_URLThe final URL of the /mcp endpoint
ALLOWED_HOSTSThe hostname of the container app
MASK_IDENTIFIERStrue
REQUIRED_ROLEThe app role from step 1 (more in step 7)

The public URL is only known after the first deployment. So you deploy once with a placeholder and then set PUBLIC_URL and ALLOWED_HOSTS with az containerapp update.

az group create --name <rg> --location westeurope
az containerapp env create --name <env> --resource-group <rg> --location westeurope

az containerapp up \
  --name <app> \
  --resource-group <rg> \
  --environment <env> \
  --source . \
  --ingress external \
  --target-port 8000 \
  --env-vars "TENANT_ID=<tenant-id>" "AUDIENCES=api://<app-id>,<app-id>" "MASK_IDENTIFIERS=true" "PUBLIC_URL=https://placeholder.invalid/mcp"

az containerapp up builds the image from the Dockerfile in Azure Container Registry. That means the image is built for linux/amd64 even on an Apple Silicon Mac. It also creates the registry if you do not name one (az containerapp up).

My first build failed. The Dockerfile used COPY --chmod=0644 server.py ./ and the build log said the --chmod option requires BuildKit. The build in the registry uses the classic builder. I replaced the line with COPY server.py ./ and RUN chmod 0644 server.py. Then it worked.

Now set the real values and keep one replica running.

az containerapp update \
  --name <app> \
  --resource-group <rg> \
  --min-replicas 1 \
  --set-env-vars "PUBLIC_URL=https://<your-app>/mcp" "ALLOWED_HOSTS=<your-app>"

Scale to zero: By default Container Apps scales to zero and you pay nothing while idle. The first call after a pause has to wake the app up. For a demo I set the minimum to one replica so the first tool call does not time out.

A request to /healthz returned 200. The new revision showed Running and Healthy.

Step 3: Check the server without Foundry

Before Foundry enters the picture I called the server myself. That separates server problems from Foundry problems.

CallResult
No token401
Azure CLI token for the Azure Resource Manager audience401 with invalid_token
Wrong Host header404

The second row is the audience check. A perfectly valid Microsoft token for another audience does not get in. The third row is not my server at all: the ingress of Container Apps routes by host name and rejects unknown hosts itself. The request never reaches the app.

Step 4: Create the connection and the agent

For the connection I used the Foundry portal, the way the docs describe it: Build, Tools, Connect a tool, Custom, Model Context Protocol. The fields:

FieldValue
Namewhoami-mcp
Remote MCP Server endpointhttps://<your-app>/mcp
AuthenticationMicrosoft Entra
TypeProject Managed Identity
Audienceapi://<app-id>

Then I created an agent with a model deployment, added the tool and asked: “Call the whoami tool and show me the full result.”

Two defaults to know. require_approval is always, so Foundry shows an approval before every call. And allowed_tools limits the agent to the tools you name. The docs show the same thing in code. I did not run this snippet because I used the portal.

from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition, MCPTool

with AIProjectClient(endpoint=PROJECT_ENDPOINT, credential=DefaultAzureCredential()) as project:
    tool = MCPTool(
        server_label="whoami",
        server_url="https://<your-app>/mcp",
        require_approval="always",
        allowed_tools=["whoami"],
        project_connection_id="whoami-mcp",
    )
    agent = project.agents.create_version(
        agent_name="WhoAmIAgent",
        definition=PromptAgentDefinition(
            model="<your-model-deployment>",
            instructions="Call the whoami tool when asked who is calling.",
            tools=[tool],
        ),
    )

The docs say project_connection_id is the ID of the connection but their samples pass the name. The snippet above uses the name whoami-mcp. I created the connection in the portal and did not run this code, so check which of the two your SDK version expects.

After the tool was created the portal showed a note that the AI gateway is not enabled. That is an optional governance layer based on Azure API Management. It sits between Foundry and your MCP server and can apply rate limits and IP filters (Governance for MCP tools). The docs describe it as a preview and it is not required. I left it off.

Step 5: Call it without a role

I had not assigned any role yet. This is what the server answered.

{
  "callerType": "application",
  "objectId": "cc45e31e...",
  "tenantId": "e44d86aa...",
  "clientApp": "569b32f3...",
  "audience": "api://1187b5ac...",
  "issuer": "https://sts.windows.net/e44d86aa.../",
  "expiresAt": "2026-10-26T16:25:52+00:00"
}

The call worked. There is no appRoles field in the answer. Entra issued a token for my API to the Foundry identity although no role was assigned. My server accepted it because it had checked signature, issuer, audience and expiry. All four were fine.

Key insight: Authentication tells you who is calling. The app role is what you authorize. Without a role check on the server any workload in your tenant that can get a token for your audience can call the tool.

Step 6: Give the Foundry identity the app role

Which identity calls your server? It depends on the connection type. With the project managed identity it is one identity for all agents of the project. The Microsoft sample template assigns the app role to the managed identity of the Foundry project (Deploy a remote MCP server). The assignment is one call to Microsoft Graph. The Graph docs recommend the appRoleAssignedTo endpoint of the API (appRoleAssignedTo).

az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/<api-service-principal-id>/appRoleAssignedTo" \
  --headers "Content-Type=application/json" \
  --body '{"principalId":"<project-managed-identity-id>","resourceId":"<api-service-principal-id>","appRoleId":"<app-role-id>"}'

The call needs the Graph permission AppRoleAssignment.ReadWrite.All or an Entra role that includes it. I found the ID of the project managed identity with az cognitiveservices account project show and the query identity.principalId.

Then I called the agent again. The answer still had no role. The value of expiresAt was exactly the same as before. The old token was still in use. I do not know whether a cache or a propagation delay caused it. What worked was a second connection with the client ID alone as audience. For Entra that is a different resource string and it produced a fresh token.

{
  "callerType": "application",
  "objectId": "cc45e31e...",
  "tenantId": "e44d86aa...",
  "clientApp": "569b32f3...",
  "appRoles": ["Mcp.Tools.ReadWrite.All"],
  "audience": "1187b5ac...",
  "issuer": "https://sts.windows.net/e44d86aa.../",
  "expiresAt": "2026-10-26T16:36:51+00:00"
}

Key insight: Role changes do not reach a token that was issued earlier. If appRoles is missing right after the assignment you probably look at an old token. Plan for that when you test.

Step 7: Let the server enforce the role

My demo server now has an optional setting REQUIRED_ROLE. If it is set a valid token must also carry this role. Otherwise the server answers 403. It is a small change because the MCP SDK already supports required scopes and my verifier writes the app roles into the scopes.

auth=AuthSettings(
    ...,
    required_scopes=[settings.required_role] if settings.required_role else None,
)

With the setting on I called two agents. The first still held the old token without a role. The second held the fresh token with the role.

AgentTokenResult
First agentOld token without role403
Second agentFresh token with role200

In the server log the same two calls look like this:

"POST /mcp HTTP/1.1" 403 Forbidden
"GET /mcp HTTP/1.1" 403 Forbidden
"POST /mcp HTTP/1.1" 200 OK

Foundry showed the same 403 in a long, general message. It lists three possible causes: missing permission, network and the access rules of the server. The real reason, a missing app role, is not in the message. The server log shows only the status code.

Foundry error message Access denied when connecting to the MCP server while invoking tool whoami with HTTP 403 Forbidden
A valid token without the app role ends in a 403. The message in Foundry stays generic and does not name the missing role

Key insight: 401 means the token is not valid. 403 means the token is valid and something is missing. In this setup that was the app role. If you see the general HTTP 403 Forbidden message from Foundry, check the app role first.

Agent identity or project managed identity?

I repeated the test with the agent identity. For this I created a third connection with the type Agent Identity and attached it to a new agent. The docs explain neither how to give a custom API role to an agent identity nor what the token looks like. This is what I saw on 25 October 2026.

FieldAzure CLI login (last post)Project managed identityAgent identity
callerTypeuserapplicationapplication
userNamemasked user namenonenone
delegatedScopesuser_impersonationnonenone
objectId421d1295...cc45e31e...d5e472cf...
appRolesnoneMcp.Tools.ReadWrite.AllMcp.Tools.ReadWrite.All

The agent identity has its own object ID, different from the project managed identity. The same Graph call assigned the role to it. To get its object ID I switched off the masking of the demo server for a moment and read the value in my own terminal. After the assignment a new conversation with the same agent showed appRoles in the answer.

Two tokens compared, one without a role leading to 403 and one with the role leading to 200 after the server checks signature, issuer, audience and app role
Only the app role in the roles claim lets the call through. Signature, issuer and audience alone are not enough

Key insight: With the agent identity you can give two agents of one project different access to the same server. With the project managed identity all agents share one caller. For the server both look the same: a workload without a person.

Optional: add the user with OAuth passthrough

If the tool must know the user switch the connection to OAuth identity passthrough with your own app registration. The user signs in once and the consent is stored per tool name per project. The server then sees a user token. I keep this for a later post.

Common pitfalls

SymptomLikely causeFix
Cannot pass Microsoft token to untrusted MCP endpoint.The audience is a Microsoft oneUse the Application ID URI of your own API
401 from the serverWrong audience or issuer versionDecode the token and compare aud and iss with AUDIENCES
HTTP 403 Forbidden in a long general messageThe token is valid but the app role is missingAssign the role to the identity and make sure you hold a fresh token
421 Misdirected Request with Invalid Host headerALLOWED_HOSTS does not contain the public host nameSet it to the host name of the container app. The server sees it unchanged and without port
404 for a request with a wrong Host headerThe ingress routes by host nameNothing to fix. The request never reaches your app
appRoles is empty right after the assignmentThe token was issued before the assignmentGet a fresh token. A different audience string forced one for me
Build fails with the --chmod option requires BuildKitThe build in the registry uses the classic builderUse COPY and RUN chmod instead of COPY --chmod
The first call times outThe app scaled to zeroSet the minimum replicas to one
The tool never runsrequire_approval is alwaysSend the approval or change the setting

The 421 looks different from the 403 in Foundry. This time the response body names the cause.

Foundry error message with status code 421 Misdirected Request and the response body Invalid Host header
A wrong ALLOWED_HOSTS value makes the server reject the host name of the Container App with a 421

Clean up

The demo is public while it runs. Delete everything when you are done. In my setup the Foundry resource and the project lived in the same resource group, so one command removes all of it.

az group delete --name <rg> --yes --no-wait
az ad app delete --id <app-id>

Azure keeps a deleted Foundry resource for a while (soft delete). If you want to reuse the name right away purge it with az cognitiveservices account purge.

az cognitiveservices account purge --name <foundry-resource> --resource-group <rg> --location <location>

The whole cleanup took about 29 minutes in my case. Most of that was waiting for the Foundry resource. Afterwards the resource group was gone and no soft deleted resource or app registration was left.

Practical checklist

  1. Create your own API: Application ID URI, service principal and an app role for applications.
  2. Deploy the container: External ingress, correct environment variables and at least one replica.
  3. Check the server without Foundry: /healthz, a call without token and a call with a token for another audience.
  4. Assign the app role: Give it to the identity that Foundry uses and plan for a fresh token.
  5. Create the connection: Use the right authentication type and your audience.
  6. Limit the tool: Use allowed_tools and keep the approval on while you test.
  7. Enforce the role on the server: Do not rely on the audience alone.
  8. Read the whoami answer: Confirm a workload and the role.
  9. Delete the resources: Do this after the test.

Key takeaways

  1. Foundry only calls remote MCP servers. Container Apps and Functions are the documented hosts.
  2. Both hosts are stateless only.
  3. Bring your own app registration. A Microsoft audience is blocked for custom servers.
  4. A valid token is not authorization. Check the app role on the server.
  5. Role changes need a fresh token.
  6. The agent identity has its own object ID and can hold its own role.
  7. Entra authentication gives the server a workload and no person.
  8. Set ALLOWED_HOSTS to the host name of the app and delete the resources afterwards.

whoami now shows a workload and no person. In a later post I want to add the user with OAuth identity passthrough and my own OAuth client, and see what the server gets then.

Sources

  • Set up authentication for Model Context Protocol (MCP) tools (Microsoft Learn)
  • Connect to Model Context Protocol servers (Microsoft Learn)
  • Deploy a remote MCP server to Azure Container Apps for Microsoft Foundry (Microsoft Learn)
  • Agent identity concepts in Microsoft Foundry (Microsoft Learn)
  • Governance for MCP tools with an AI gateway (Microsoft Learn)
  • Add app roles to your application (Microsoft Learn)
  • Assign an app role: servicePrincipal appRoleAssignedTo (Microsoft Learn)
  • Ingress in Azure Container Apps (Microsoft Learn)
  • Set scaling rules in Azure Container Apps (Microsoft Learn)
  • az containerapp up (Microsoft Learn)
  • Which Agent, on Whose Behalf? Authenticating MCP Tools in Microsoft Foundry (frezz-tech.de)
  • foundry-mcp-auth-whoami-demo (GitHub)

Tags:

AI AgentsAzureAzure Container AppsMCPMicrosoft Entra
Author

Alexander Dierkes

Follow Me
Other Articles
Three users share one caller identity on the left while OAuth passthrough gives each user their own token on the right
Previous

Which Agent, on Whose Behalf? Authenticating MCP Tools in Microsoft Foundry

A user consents, a Foundry connection carries the user token to an MCP server and the server sees callerType user
Next

From Workload to Person: OAuth Identity Passthrough for Your Own MCP Server in Microsoft Foundry

One Comment
  1. From Workload to Person: OAuth Identity Passthrough for Your Own MCP Server in Microsoft Foundry | Frezz Tech says:
    02.11.2026 at 18:31

    […] my last post the whoami server ran on Azure Container Apps and a Foundry agent called it with a Microsoft Entra […]

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest Posts

  • From Workload to Person: OAuth Identity Passthrough for Your Own MCP Server in Microsoft Foundry
  • Hosting Your Own MCP Server for Microsoft Foundry on Azure Container Apps
  • Which Agent, on Whose Behalf? Authenticating MCP Tools in Microsoft Foundry
  • Which Identity Does Your Foundry Agent Really Use?
  • Running a 70B model on a laptop with AirLLM: what it costs in time and where Microsoft Foundry fits

AI Agents Authentication Azure Azure Container Apps Azure OpenAI Foundry Local Governance Identity Passthrough Local AI MCP Microsoft Entra Microsoft Foundry OAuth

Resources

About | Imprint

Disclaimer

Opinions expressed here are my own and may not reflect those of others. Unless I'm quoting someone, they're just my own views.

© 2026 Frezz Tech - All rights reserved by Alexander Dierkes
Independent technology blog. Not affiliated with Microsoft.