Access Is Not Authority: Four Rules for the MCP Layer on Azure Functions

“You don’t pick MCP or an API. You stack them.” That line opened a LinkedIn post earlier, with a tidy picture: AI agent → MCP server → your existing API, and four rules for the MCP layer. Keep auth in the API. Build tools around tasks. Expose less. Label reads and writes.

I agreed with all four, and I had already built most of them in my MCP vs API series. What made me open an editor again was the comment thread. One reader asked what a single agent gains from MCP when the API already does the job. Another said that before authority there is provenance: is the offer the agent is about to book even genuine? A third added that access is not authority, and that a successful tool call is not a successful business outcome.

So this post does three things. It puts the four rules on Azure Functions. It adds the three concerns from the comments. And it measures the one rule the Azure Functions MCP extension cannot keep for you.

The sample is at github.com/steefjan1/mcp-layer-rules-azure.

The stack, on Azure

Two Function apps, one storage account, one Entra app registration.

The API (func-stayapi) is an HTTP-triggered .NET 8 app behind Functions built-in authentication. It owns everything that decides something: listings, quotes, bookings, cancellations, roles, idempotency, audit. The MCP server (func-staymcp) is a second Functions app with the MCP tool trigger. It owns tool names, descriptions, a confirmation flag, and one signature check. It has no business rules, and it has no table it writes to.

That split answers the first commenter. With one agent and one API, the MCP server is not improving the API. It is the door the model reads, and it is where you decide what the model may even see. The API stays the door software calls. Same kitchen, two doors, which is the whole point of the series this post belongs to.

Rule 1: the MCP server gets its own token

The infographic says the spec forbids passing the user’s token through. That is right: the MCP authorization spec is explicit that a server must not forward the token it received from the client to an upstream API. On Azure the natural way to obey is a managed identity.

The MCP app has a user-assigned identity. On every API call it asks DefaultAzureCredential for a token with the API’s app ID URI as the scope. The API app has built-in authentication switched on in Bicep, with the app registration as the audience, so the platform validates issuer, audience, signature and expiry before the function runs. The function reads roles from X-MS-CLIENT-PRINCIPAL and nothing else.

There is a detail the Functions extension makes easy. The MCP tool trigger hands your function a ToolInvocationContext with no caller identity at all. There is no user token in scope to forward by accident. Rule 1 is kept partly because the trigger does not give you the rope.

Rule 2 and rule 3 pull in opposite directions

The API has six routes. The MCP server has four tools: find_stays, quote_stay, book_stay, cancel_stay. GET /bookings/{id} and the public key endpoint are not tools. The agent gets the booking back from book_stay, and the key is something the MCP server fetches, not the model.

Then I measured, because rule 3 is a number. measure/list_tools.py runs tools/list against the server and counts the tokens of each definition the client will put in front of the model. The four task-shaped tools cost 589 tokens in this sample (cl100k_base, live tools/list from the deployed server). The same six routes as six endpoint-shaped tools with one-line descriptions cost 384.

Fewer tools did not mean fewer tokens. The task shape spends its tokens on when-to-call prose: call quote_stay before book_stay, pass the signed quote unchanged, get a yes before confirm=true. The endpoint shape leaves that sequence, and the idempotency key, to the model. I will take 205 tokens over a model that invents its own booking protocol, but it is worth knowing which rule you are paying for. The script fails CI over a budget, so the surface cannot grow quietly.

Rule 4: the label is not on the wire

This is the finding. The infographic says to label reads and writes with readOnlyHint and destructiveHint, and it is careful to add that they are hints, so keep a confirmation on anything that pays or deletes.

The Azure Functions MCP trigger attribute takes a tool name and a description. That is it. There is no parameter for annotations, and the measurement script confirms what tools/list returns: no annotations object on any of the four tools. A client that uses annotations to decide what runs without asking sees four unlabelled tools and has to assume the worst, which in fairness is what the MCP spec tells clients to do.

So the sample puts the label where this extension lets it live. The description starts with “Read-only”, “Writes” or “Destructive”. The two writing tools take a confirm argument and refuse with prose when it is not true. The host.json instructions say the same thing once for the whole server. None of that is a security control, and the comment thread is right that annotations are not either. The control is the next section.

What the comments added: authority, provenance, recovery

Authority. Three app roles on the API registration: Stays.Read, Bookings.Create, Bookings.Cancel. grant-roles.ps1 assigns them to the MCP identity, and that script is the only place where “what may the agent change” is decided. Run it with Stays.Read only and book_stay becomes a tool the agent can reach and the API refuses, every call, with a 403 the MCP server renders as prose: this action is not available through the assistant. Access is not authority. The REST door keeps working for a caller that holds the role.

Provenance. The booking commenter’s point was that nothing in MCP or the API proves the offer the agent acts on was issued by the host and not changed on the way. The sample’s API signs every quote with ECDSA P-256 over a canonical body (listing, dates, guests, total, expiry) and publishes the public key at /.well-known/stay-keys. The MCP server verifies the signature before it calls book_stay, fails closed, and caches the key for five minutes, the same revocation window the commenter’s protocol uses. The API verifies again on POST /bookings. Change the total between quote_stay and book_stay, whether the model did it or a prompt injection did, and the write call is never made.

Recovery. POST /bookings requires an Idempotency-Key. The MCP server derives it from the quote id, so a retry of the same quote is the same booking. The API claims the key in Table storage before it writes, returns the existing booking with Idempotent-Replayed: true on a repeat, and loses a race cleanly. An audit table gets a row per stage: requested, denied or rejected, authorized, attempted, completed, replayed. That is the “what was requested, approved, attempted, and actually completed” record the comment asked for, in the kitchen, keyed by quote.

What the first run against Azure taught me

Two things I did not plan for, both worth knowing before you build the same.

The MCP extension parses arguments before it binds them. I declared checkIn as a string and the model sent "2026-12-05". The function received 12/05/2026 00:00:00 +00:00, the ToString() of a DateTimeOffset the extension had helpfully made on the way in. The API, which demands yyyy-MM-dd, refused it. The fix is small (the tool normalises the date before it calls the API) but the lesson is bigger: the door is allowed to be forgiving, the kitchen is not, and you only find out where the extension is “helping” by running it.

The audit row names the agent, not the user. createdBy on a booking made through the MCP door is the object id of the MCP app’s managed identity, because that is who called the API. The user who said yes in VS Code is nowhere in the chain. That is rule 1 working exactly as written, and it is also a gap: if you need “which human approved this” in the record, the MCP server has to carry it as data (a claim from its own authenticated client, written into the request body) because the API will never see a user token. I covered what it takes to authenticate that client in the 401 post.

Where this is the wrong answer

If one deterministic system calls your API, add nothing. MCP earns its place when a model chooses the call, or when more than one agent needs the same tools with the same descriptions.

If your clients are end-user assistants that expect OAuth, the Functions system key on the MCP endpoint is the wrong door lock. Switch on built-in MCP authentication on the MCP app as well; I walked through that, and the invalid_target wall behind it, in What a 401 Means to an MCP Client.

If you need annotations on the wire today, this extension version does not give them to you. API Management’s MCP export and the MCP C# SDK hosted on your own container both can. That trade is a post of its own.

And a signed quote proves the price is the one the host issued. It does not prove the host is honest. Provenance answers “is this real”, not “is this a good deal”.

What to take away

The infographic’s four rules hold on Azure, and three of them are cheap: a managed identity, a tool-per-task layout, and a script that counts tokens. The fourth, labelling, is where the platform leaves a gap, and the comment thread had already told me what fills it. Not a hint. A role on the API, a signature on the thing being acted on, and an idempotency key on the write.

The MCP layer decides what the model sees. The API decides what happens. Keep it that way and the diagram stays small.

Sources

Don’t Build Around Today’s Model. Build for the AI Control Plane

Microsoft isn’t just shipping AI models. It is building toward a model-agnostic AI control plane. The bigger story behind the recent Microsoft Foundry updates is not another model launch. It is the architecture direction. Once you see it, the individual announcements fall into place as layers of one stack:

AI Control Plane → Model Router → Agents → RAG/Knowledge → MCP/Tools → Enterprise Systems

The AI control plane takes shape

Two August updates make the direction concrete. First, the model router update expanded the router to 28 regions for global standard and 21 data zone regions. The supported pool now includes Anthropic Claude Opus 4.8 and the GPT-5.6 family, while deprecated models such as DeepSeek-V3.1 and the gpt-5-chat line were pruned. Second, Foundry added DeepSeek-V4-Flash-0731 and NVIDIA Nemotron 3.5 Lightning to the catalog, each available through multiple deployment paths: Direct from Azure, Fireworks on Foundry, or the Hugging Face collection on managed compute.

I covered the router update in more detail on InfoQ, including the caveats around behavioral stability. Here, I want to focus on what the updates mean for your architecture.

Neither update is spectacular on its own. Together, however, they show a platform where models arrive, improve, and retire underneath a stable endpoint. Your application keeps calling the same integration while the pool refreshes. That is control plane behavior, not model shipping.

Agent ≠ Model

Here is the key idea for enterprise architects: an agent is not a model. An agent is an orchestration unit with instructions, knowledge, and tools. The model is a swappable dependency underneath it.

The model router makes that separation operational. It selects a model per request, optimizing for quality, cost, and latency within the regions your governance allows. You can run it in balanced, quality, or cost mode, and you can restrict routing to an approved subset of models. Moreover, every response includes a model field that shows which model handled the request, so the routing decisions leave an auditable trail.

Microsoft frames this as a hill climb: model selection as a continuous, measured loop rather than a one-time decision. In an ecosystem where the frontier moves monthly, a hardcoded model choice goes stale fast. An agent bound to the control plane instead of a specific model can evolve without a rebuild every time the leaderboard changes.

What this means for your architecture

For enterprise architects, the practical guidance follows directly. Treat model selection as configuration, not code. Put governance, observability, and policy at the control plane layer, because that is where they survive model churn. Ground agents in your own knowledge through RAG, and connect them to enterprise systems through MCP and tools. Consequently, each layer can evolve at its own pace. The stack outlives any single model.

Where this is the wrong answer

Model-agnostic routing is not free, and it is not always right. If your workload requires reproducible behavior, for example in regulated decision flows, a pinned model version beats a router that refreshes its pool automatically. Your evaluations were run against a specific model; a silent pool refresh invalidates them until you re-run. There are operational caveats too: Anthropic models still need to be deployed separately before the router can reach them, and routing modes take time to propagate. Finally, a single well-tuned small model per agent remains the simpler option for narrow, high-volume tasks. Routing adds a layer you must monitor. Only accept that cost when model diversity actually pays for itself.

Closing thoughts

The direction is clear even where the details will shift. Models are becoming interchangeable parts. The durable investment is the control plane: routing, governance, evaluation, and the connective tissue to your enterprise systems. So don’t build around today’s model. Build for the control plane, and let the models come and go.

What’s your approach: one model per agent, or model-agnostic agents?

Hosting MCP Servers on Azure Functions: GA vs. Preview

The Model Context Protocol (MCP) has become the standard way AI agents and models interact with external systems. If you are building something on Azure and need to expose tools to an AI client, the question is no longer whether to use MCP; it is which hosting option to choose.

Azure Functions supports three distinct approaches. Two of them host MCP servers directly. The third uses message queues instead of MCP calls altogether. Each reflects a different set of trade-offs. This post maps them out so you can pick the right one before you write any code.

Why use Azure Functions for hosting MCP servers?

The doc makes the case concisely: Functions scales efficiently to handle demand and provides binding extensions that simplify AI integration. Both matter for MCP hosting specifically.

MCP servers are called on demand. An AI client sends a request, the server responds, and then goes quiet. That is a bursty, event-driven pattern. Flex Consumption and Elastic Premium both handle it well. Flex Consumption gives you scale-to-zero billing for bursty tool workloads. Elastic Premium gives you pre-warmed instances, predictable latency, and VNet integration for tools that need to reach internal APIs and databases.

Managed identity is the other key piece. Your MCP server likely needs to call downstream Azure services: blob storage, Cosmos DB, and Azure AI Search. With managed identity, you do not need to manage credentials inside your function code, and you do not need to pass secrets through your MCP configuration.

The three ways to host MCP servers on Azure Functions

Option 1: MCP binding extension (GA)

This is the right default for most teams. The binding extension lets you build an MCP server using the standard Functions programming model triggers, bindings, local development with Core Tools, and deployment via azd. You annotate a function with McpToolTrigger, and it is automatically exposed as an MCP tool.

The comparison table from the Microsoft Learn documentation makes the feature set clear:

FeatureMCP binding extension
Support levelGA
Programming modelFunctions, triggers and bindings
Stateful executionSupported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsNone
ImplementationMCP binding extension

Stateful execution support is the binding extension’s meaningful advantage over the self-hosted SDK option. If your MCP server needs to maintain session state across tool calls, for example, a multi-turn data retrieval scenario, only the binding extension handles that today.

The quickstart template (remote-mcp-functions-dotnet) is a useful starting point. It scaffolds the project via azd init, runs locally with the Azurite storage emulator, and includes a .vscode/mcp.json file that wires the local endpoint directly into GitHub Copilot’s agent mode for testing. That local-to-Copilot loop is genuinely quick; you can test a tool call from Copilot chat without deploying anything.

Before you build: a toolchain issue to know about

There is a confirmed bug in Microsoft.Azure.Functions.Worker.Sdk (all versions to 2.0.7) where the auto-generated WorkerExtensions.csproj is hardcoded to net6.0. The MCP extension package requires net8.0, so the build fails out of the box with NU1202: Package is not compatible with net6.0. The Microsoft Learn quickstart does not mention this.

The workaround that actually works has three parts: add an extensionBundle to host.json so the host loads MCP support at runtime rather than through the build-time generator; mark all extension PackageReference entries with PrivateAssets="all" so their build targets don’t re-trigger the generator; and use 1.0.0-preview.3 rather than 1.0.0 or later preview versions, because the generated project still targets net6.0 and needs a package version it can restore against that framework. The companion repo has all three changes applied and a detailed explanation in the README.

The McpToolTrigger attribute is the core mechanism. Here is what the minimal C# version looks like from the quickstart:

[Function(nameof(SayHello))]
public string SayHello(
[McpToolTrigger(HelloToolName, HelloToolDescription)] ToolInvocationContext context
)
{
logger.LogInformation("C# MCP tool trigger function processed a request.");
return "Hello I am MCP Tool!";
}

The trigger attribute registers the function as an MCP tool and handles the protocol negotiation. You write the tool logic; the extension handles the MCP wire format.

Use this when: you are building a new MCP server and do not have an existing codebase using MCP SDKs. It is the fastest path from zero to a deployed, governed MCP server on Azure.

Avoid it when: you already have an MCP server built with an official MCP SDK and want to lift it into Azure without rewriting it in the Functions model.


Note: Microsoft has samples for this option. You can find all the MCP extension samples at aka.ms/remote-mcp

Option 2: Self-hosted MCP servers via official MCP SDKs (preview)

This option lets you take an existing MCP server built with the official MCP SDKs and host it on Azure Functions without rewriting it as a binding-extension-style function app. Functions acts as the hosting runtime; your MCP SDK code runs via custom handlers.

FeatureSelf-hosted MCP servers
Support levelPreview
Programming modelStandard MCP SDKs
Stateful executionNot currently supported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsStreamable HTTP transport
ImplementationCustom handlers

Two constraints define whether this option is viable for you right now.

  • First: Streamable HTTP transport is required. The self-hosted option does not support Server-Sent Events (SSE) transport. Suppose your existing MCP server relies on SSE, which many early implementations did; you need to migrate to Streamable HTTP before hosting on Functions. That is a non-trivial change for an established codebase.
  • Second: stateful execution is not supported during preview. If your MCP server is stateless most tool servers are this is not a problem. But if you need session continuity across calls, stay with the binding extension.

The documentation also flags that configuration details for self-hosted MCP servers change during the preview period. That means operational overhead: you may need to adjust configuration as the feature evolves. Factor that into a production timeline.

The sample (Weather server) shows the self-hosted pattern working correctly for a stateless tool. It is a useful reference for understanding the custom handler wiring, but it does not represent a feature-complete production deployment yet.

Use this when: you have an existing MCP server built with official MCP SDKs, it uses Streamable HTTP transport, it is stateless, and you want to run it on Functions without adopting the binding extension model.

Avoid it when: you need stateful execution, you rely on SSE transport, or you are building from scratch. In those cases, the binding extension is the better path.

Note: There is another approach for hosting official MCP SDK-based servers. This approach is about hosting MCP in Connector Namespace, which was announced in public preview at Build.


Option 3: Queue-based Azure Functions tools

This is the option that gets overlooked because it does not follow the MCP pattern at all, and that is sometimes exactly what you want.

Instead of an AI agent calling your function via the MCP protocol, the agent sends a message to a queue (Service Bus or Azure Storage Queue), and a queue-triggered function picks it up asynchronously. Foundry provides Azure Functions-specific tooling for this pattern.

The Microsoft Learn documentation lists the scenarios where this is the right choice:

  • Reliable message delivery and processing
  • Decoupling between AI agents and function execution
  • Built-in retry and error handling
  • Integration with existing Azure messaging infrastructure

That last point is often what decides it in enterprise contexts. If your integration platform already uses Service Bus for order processing, claims workflows, or event-driven pipelines, the queue-based tool pattern slots into that infrastructure without adding a new protocol layer. Your AI agent becomes another message producer; your function is another consumer. Operations teams already know how to monitor and manage it.

The decoupling is also genuinely useful for long-running tool operations. An MCP call is synchronous from the agent’s perspective: it sends a request and waits for a response. A queue-based call lets the agent fire and move on; the function processes in the background and the result arrives via a separate callback or polling mechanism. For operations that take seconds or minutes document processing, batch retrieval, report generation that asymmetry matters.

Use this when: the tool operation is long-running, you need reliable delivery with built-in retry, you are integrating with existing Azure messaging infrastructure, or you want to decouple the agent from function execution timing.

Avoid it when: the agent needs a synchronous response immediately, or you are connecting to AI clients that expect the MCP protocol specifically.

Note: For long-running processes or multi-step workflows, you might also want to consider Durable Functions instead of “vanilla” Functions. Durable Functions provides built-in state persistence and automatic retries, which is helpful when long-running or multi-step workflows fail mid-execution because of things like unreliable network connectivity. Because state is persisted during execution, Durable Functions can rebuild local state up to the point of failure and continue executing from there instead of from scratch. 


How to choose

SituationOption
Building a new MCP server from scratchBinding extension
Need stateful tool executionBinding extension
Have an existing SDK-based MCP server (Streamable HTTP, stateless)Self-hosted SDK
Need async, decoupled, fault-tolerant tool executionQueue-based
Integrating with existing Service Bus infrastructureQueue-based
Building for GitHub Copilot or VS Code agent modeBinding extension
Production deadline in the next quarterBinding extension or queue-based (avoid preview)
I need pre-warmed instances and predictable latencyElastic Premium plan

The preview constraint is the most important practical point here. If you are building for production and your timeline does not allow for configuration changes mid-project, self-hosted MCP servers are not ready. The binding extension and queue-based options are both GA and stable.

APIM as the governance layer

Whichever option you choose, consider putting APIM in front of your MCP server endpoint. This gives you rate limiting, authentication policy, token quota management, and a single point for logging and monitoring across all MCP tool calls.

One practical caveat from real deployment experience: the Azure AI Foundry Agent Service SDK routes LLM calls directly to Azure OpenAI, bypassing APIM entirely. Tool calls to your MCP server do pass through APIM, but model calls do not unless you use the standard OpenAI SDK instead. The Citadel Platform series covers this in detail, including how to structure your APIM policies to handle the tool-call traffic that does flow through it.

What comes next

The next post goes deeper into the serverless agents runtime, the preview programming model that lets you define event-triggered agents as function apps, with .agent.md files, agents.config.yaml, and remote MCP server connections declared in mcp.json.

Up next: The Azure Functions Serverless Agents Runtime: What It Is and When to Use It