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

MCP vs RAG vs Skills Is the Wrong Fight. Governance Is the Real One

A LinkedIn infographic did the rounds last week: “MCP vs RAG vs Skills”. Three bands, three colors, and a closing line that said the layers are not alternatives but parts of one system. In short, MCP connects, RAG grounds, Skills execute. The post then ended with a sentence most readers scrolled past: “Very few know how to govern them.”

That last sentence is the interesting one. The rest of the picture is mostly right, but three labels deserve a correction. So this post does both: it fixes the labels, lands each layer on Azure with working code, and then answers the question the infographic raised and left open. What governs each layer?

The companion repo is at github.com/steefjan1/mcp-rag-skills-azure. One agent, one job (filing an expense report), all three layers, deployed with azd up.

Three corrections before we build

RAG grounds, it does not cure

The infographic says “No hallucinations. No outdated context.” Retrieval puts the right passages in front of the model. However, the model can still misread them, blend two sections, or answer from memory when the passages are thin. RAG is grounding. Grounding lowers the odds, but it does not remove them, and a governance story that assumes it does will be surprised.

MCP writes as well as reads

The picture shows MCP fetching data from Slack, a vector database and a search engine. That is fine as far as it goes, but an MCP tool can also create, update and delete. In the sample, for example, two of the three tools read and one writes. The write is where governance starts to matter, so treating MCP as only “the connection layer” hides the most important call.

Skills are procedure, not “the execution layer”

This is the one I would push hardest. An agent skill, as the Agent Skills spec defines it and as Microsoft Agent Framework implements it, is a folder: a SKILL.md with instructions, optional reference files, optional scripts. First, the agent sees a one-line description. Then it loads the full instructions only when the task matches, reads references only when the instructions point to them, and finally runs a script only when the procedure says so. Four stages of progressive disclosure, and execution is the last and rarest stage. Most of a skill is procedure, not code. As a result, calling it the execution layer makes people reach for skills when they need a tool, and for tools when they need a procedure.

So the three layers are three kinds of context. RAG adds knowledge, MCP adds reach into systems, and Skills add procedure. The model decides which to use, but none of the three decides what the model may do. That fourth thing is governance, and it is the part the infographic could not draw.

One agent, three layers, on Azure

The sample is an expense assistant. An employee says “file a report for alice: train to Stockholm 1450 SEK, dinner 85 EUR”. Watch the order of calls.

The three pieces

The agent runs on Microsoft Agent Framework (Python) against a Foundry project through FoundryChatClient. Three things are wired into it.

Knowledge: Azure AI Search. Three markdown policy documents are chunked by heading, embedded with text-embedding-3-small on the Foundry resource, and then pushed into an index with a vector field. The agent gets one function tool, search_policy, which runs a hybrid query and returns passages with their source file. As a result, the model can quote the meal limit and say where it came from.

Connection: a remote MCP server on Azure Functions. Three MCP tool triggers live in a Python v2 function app on Flex Consumption: get_exchange_rate, list_expense_reports, and submit_expense_report. The agent connects with MCPStreamableHTTPTool over streamable HTTP, sending the mcp_extension system key in a header. In other words, it is the same shape as the server in my MCP vs API post, with a different domain.

Procedure: a skill folder. agent/skills/expense-report/ holds a SKILL.md with five steps (collect lines, convert currency, check policy, validate, confirm and submit), a reference file with the numeric limits, and a validate_report.py script that applies them deterministically. SkillsProvider.from_paths picks it up.

The request, step by step

Now the request. The skill description matches, so the agent calls load_skill (1). The procedure says convert foreign currency first, so get_exchange_rate (2) turns 1450 SEK into 127.60 EUR. Step three says check policy, so search_policy (3) returns the meals section. Step four then runs validate_report (4), which asks me for approval and reports that 85 EUR for one person exceeds the 60 EUR limit. The agent stops and explains. I change the dinner to 55 EUR and say “submit”, and the agent validates again and then asks for an explicit yes, because the skill says so. Only after that yes does submit_expense_report (5) ask for its own approval and fire. Report r-777fa0, status submitted.

Five calls, three layers, one procedure holding them together. Take any layer away and the flow breaks: without the skill the agent submits before validating, without the index it invents a limit, and without MCP it has no rate and nowhere to submit.

What the first two runs taught me

That clean trace is the third run. The first two are more useful.

In the first run, the agent loaded the skill, converted the currency, searched the policy, and then did the validation arithmetic itself. No run_skill_script, no approval prompt, and a correct answer. Correct by luck, because the deterministic script is there so that the numbers do not depend on the model. A skill is advice. The model read “run the script” and decided it could do the math.

In the second run, it did call the script, but with a bare list of lines and no has_receipt field, so the validator flagged two receipt violations that did not exist. The input shape lived in the script’s docstring, and the model never sees script source. Progressive disclosure means it reads SKILL.md and nothing else. So the fix was to put the exact JSON contract into step 4 of SKILL.md, and to make the script tolerant of the shapes a model tends to send. After that, the third run went first time.

Both lessons point the same way. The procedure layer only works when the instructions carry the full contract, and even then the model can skip a step. Something outside the skill has to notice when it does. In the sample that is a --trace flag and a human reading it. In production it is the trace store and an eval that asserts the script ran.

What governs each layer

Here is the part the infographic ended on. Every control below is a real setting in the sample, and each layer also has a control I left for you.

Connection: allowlist and approval

The agent declares allowed_tools with the three names it expects. If the server grows a fourth tool next month, this agent does not see it. Discovery at runtime is a feature of MCP; however, unbounded discovery is not something you want in production. Next to it, approval_mode says the two reads never ask and the write always asks. The approval request surfaces in the agent’s response, and the app decides who answers it. In the sample that is the console. In yours it is a ticket, a Teams card, or a policy engine. What is missing: an AI gateway in front. For example, API Management can rate limit the MCP endpoint, meter tokens, and act as a kill switch, which I covered in the APIM post. One small thing that bit me: the MCP Python SDK went to 2.0 and renamed its fields, so a plain pip install mcp broke the connection at initialize until I pinned it below 2. The connection layer is a dependency like any other.

Knowledge: roles and sources

The index is reached with Entra data-plane roles, not an admin key, and every chunk carries its source file so the model can cite. Moreover, the documents come from a versioned folder in the repo, loaded by a script, which means a policy change is a pull request. What is missing: document-level access control. The sample policy is company-wide; the moment one user group may see a passage another may not, the index needs security filters and the query needs the caller’s identity.

Procedure: free to read, gated to run

The SkillsProvider exposes three tools to the model: load_skill, read_skill_resource, run_skill_script. All three require approval by default. The sample relaxes the first two, because reading instructions is safe, and keeps the third. Scripts then run through one runner function: the agent’s own interpreter, a 30 second timeout, the skill folder as working directory. That runner is the single place to put a sandbox later. What is missing: exactly that sandbox, a per-agent allowlist once skills come from a shared catalog, and the eval from the previous section that checks the procedure was followed rather than paraphrased.

Across all three: identity and traces

Identity and observability do not belong to a layer. The Foundry resource has disableLocalAuth on, so every model call carries an Entra token. Likewise, the Functions host reaches its storage with a managed identity, no shared keys. Locally the agent is you after az login; in Azure it is a managed identity or an Entra agent identity. Finally, every model call, tool call, skill load and script run becomes an OpenTelemetry span in Application Insights, with sensitive data off so prompts and arguments stay out of the trace store.

None of this is exotic. It is a list of settings. The point is that each layer has its own list, and “we use MCP” or “we have RAG” says nothing about whether any of them are set.

Where this is the wrong answer

Three layers is not always the right count.

If the knowledge is small and stable, skip the index. Instead, put the policy in the skill’s reference file and let read_skill_resource serve it. The sample could have done that, and only did not because the point was to show the seam.

If the agent never writes, MCP with an approval model is more ceremony than a plain function tool that calls the API. MCP earns its place when several agents share a server, when tools must be discovered rather than compiled in, or when the server sits behind a gateway you want to govern once.

If the procedure is one step, a skill is overhead. Skills pay off when the steps have an order, when the order matters, and when you want the procedure reviewed in a pull request instead of living in a system prompt.

And if you cannot say who the agent is, do not add any of the three. Identity first, then layers.

Closing

The infographic was right that MCP, RAG and Skills solve different parts of the same problem. Its labels were loose, but its last line was the best line. Three kinds of context feed one model, and something outside all three has to say what that model may do. On Azure, that something is a short list per layer: an allowlist and an approval mode, data-plane roles and source citations, approval on script execution and one runner to sandbox, and finally one identity and one trace store across all of it.

The repo has all of it in code. Clone it, run azd up, and then search for GOVERNANCE.


Sources and further reading

Azure Functions or Foundry Agent Service Is the Wrong Question

Enterprise teams keep asking me the same architecture question. Should agent work run in Azure Functions or in Foundry Agent Service? The question sounds like a choice between two products. It is not. It is a question about where you draw the Foundry Agent Service architecture boundary, and the honest answer changed in the last twelve months while most of the diagrams did not.

The Foundry Agent Service architecture boundary, as commonly drawn

Here is the version of the pattern I see repeated in decks, posts, and internal architecture docs.

Azure Functions sits below the line as the action layer. Tools, action handlers, data retrieval, API adapters. Stateless, discrete, testable, independently scalable. Foundry Agent Service sits above the line as the orchestration and reasoning layer. Thread state, tool coordination, evaluation, governance. The banner at the bottom says to keep orchestration in Foundry and actions in Functions.

Most of that diagram is still correct. One box in it is now wrong, and it happens to be the box the whole argument rests on.

The claim that stopped being true

The reasoning behind the pattern went like this. Azure Functions is a stateless runtime. If you build agent orchestration inside it, you end up hand building a state machine: persisting conversation turns yourself, tracking which tool calls already completed, writing your own resume logic after a crash, and owning every retry path end to end. That is real work, it is easy to get wrong, and Foundry Agent Service does it for you. So orchestration belongs above the line.

In 2025 that argument held. Then the Durable Task extension for Microsoft Agent Framework shipped, and Azure Functions acquired the thing it was said to lack.

Durable agents give you persistent sessions that survive process crashes, restarts, and scaling events. They checkpoint automatically, so completed steps do not re-execute on recovery. Sequential, fan out and fan in, and conditional routing across multiple agents all work, with context.GetAgent() pulling a registered agent into an orchestration. And the whole thing runs on the Flex Consumption plan and scales to zero.

That is a managed state machine, in Azure Functions, supported by Microsoft. The strongest paragraph in the received pattern now argues against a product that exists.

Preview status, read from the feed rather than the docs

Note the status carefully, because this is where enthusiasm outruns the facts. The core Microsoft Agent Framework packages are stable. The durable hosting packages are not. Microsoft.Agents.AI.Hosting.AzureFunctions and Microsoft.Agents.AI.DurableTask have never had a stable release. Neither Learn page carries a preview banner, but every documented install command passes --prerelease, and the feed agrees.

Building the sample turned up something sharper than the package labels. Microsoft.Agents.AI.Foundry 1.19.0 binds Azure.AI.Projects 2.1.0.0, and version 2.1.0 exists only as a beta. Pin the stable 2.0.1 underneath it and the compiler refuses with a version conflict. So the maturity question does not stop at the hosting packages. Today, the current Agent Framework Foundry integration cannot be assembled on top of a stable data plane SDK at all.

Say that out loud in an architecture review, and cite the package feed rather than the docs.

Three substrates above the Foundry Agent Service architecture boundary

Once you accept that Functions can orchestrate, the Foundry Agent Service architecture boundary stops being a layer cake and becomes a decision.

Foundry prompt agents. Configuration rather than runtime code. Foundry owns the thread state, the lifecycle, the tracing, the evaluation, and the retry behavior. This is the fastest route to a governed production agent, and it costs you the least code and the least control. When a team wants an agent in front of existing APIs and does not want to run anything, this is the answer.

Durable agents on Azure Functions. Your orchestration, in C#, with checkpointing supplied by the durable runtime. You own the deployment pipeline, the infrastructure as code, the tests, the alerting, and the pager. You get deterministic, inspectable control flow and the same operational tooling as the rest of your estate. When the orchestration has real branching, compensation, or long running steps that must survive a restart, this earns its keep.

Foundry hosted agents. Your Agent Framework or LangGraph code, shipped as a container image or a zip into Foundry’s runtime. Foundry supplies the endpoint, the scaling, a dedicated Microsoft Entra identity per agent, and session level state. You supply the code. This is the case that blurs the boundary the original diagram insists on keeping clean, because neither side owns the whole thing.

One action layer below the line

Below the line, nothing changes. Azure Functions remains the action and tool layer for all three. You surface tools over MCP at the Functions webhook endpoint, register them once in a Foundry toolbox (I covered how that moves tool authentication off the agent in my Foundry Toolboxes post), and let any substrate above discover them at runtime. The tool code does not know or care which orchestrator called it, and hardening it is ordinary API work of the kind I described in Azure Functions behind API Management.

The companion repo, and what it proves

I built a companion repo to check that this holds in practice rather than only in a diagram. It contains one action layer and three orchestrators over it, running the same order fulfillment task. You can find it at function-foundry-boundry.

All three substrates ran against the deployed action layer. The durable agent is deployed and measured, with the smoke suite passing twelve of twelve: the action layer, the idempotency guarantee, and a full orchestration ending in exactly one customer notification. Its prompt agent sibling was created against the live MCP endpoint and driven from the Foundry playground. And the hosted agent is registered and active in Foundry, with its container exercised end to end over the responses protocol self hosted; the one path I have not driven is invoking that container through Foundry’s own runtime. Getting all of this working took forty one documented findings, every one listed in the repo’s verification log, and several of them taught me more than the sample did. The repo states its coverage in the same words, because a sample that overstates what it proved is worse than no sample.

What actually differs, in code

Building all three surfaced a difference I did not expect to be so concrete.

The action layer exposes five operations: look up an order, check inventory, reserve stock, arrange delivery, and notify the customer. Two of those change state a customer can see. Reserving stock commits inventory. Notifying the customer sends a message that cannot be unsent.

In the durable agent, those two operations do not appear in the model’s tool set at all. The orchestrator calls them as activities, where the durable runtime commits them exactly once even if the surrounding agent turns replay. What the model gets is three read oriented tools and the job of producing a plan, while the runtime executes the parts that spend money.

In the prompt agent and the hosted agent, there is no activity boundary to put them behind. The model itself has to be trusted to call each one exactly once, and your protection is instructions plus an idempotency key enforced by the tool.

That asymmetry is the trade, stated in about twelve lines. It is not a bug in either approach. It is what “who owns retry and failure handling” means once you stop discussing it abstractly.

Three substrates, three failures, one shape

I did not have to wait long for the trade to show itself in data. All three substrates ran against the same action layer on the same day, with instructions that said, in each case, to notify the customer exactly once and to use the delivery slot they were given. The prompt agent fulfilled its order correctly and notified the customer twice, in two differently worded messages, because two generations each decided a message was warranted; the reservation, protected by the idempotency key rather than by instructions, did not duplicate. The hosted agent notified exactly once, and booked the delivery for the literal phrase “next available”, because with no orchestrator in the stack nobody owns the clock, so the model passed prose where a date belonged and the action layer recorded it faithfully. And the durable agent, in an early revision that let the model judge whether stock sufficed, refused a fulfillable order with twelve units on the shelf, which is why the shipped version keeps the model out of that decision entirely. Three substrates, three different failures, one shape: whatever the runtime owned, held. Whatever the model was merely instructed about, eventually did not.

One practical note for anyone following the same path. The durable types are split across three namespaces that do not follow from one another: AIAgent, AgentSession and AgentResponse<T> come from Microsoft.Agents.AI, DurableAIAgent and context.GetAgent from Microsoft.Agents.AI.DurableTask, and ConfigureDurableAgents from Microsoft.Agents.AI.Hosting.AzureFunctions. The DurableTask package is documented as the bring your own compute option, which makes it easy to leave out of a Functions project. You need it on both paths, because the hosting package does not surface the orchestration types.

The one control that survives the decision

Which brings up the idempotency key. Every substrate can retry a tool call, and every substrate can replay. So the action layer refuses to care which one is calling and enforces safety itself. reserve_stock takes a reservation key and returns the existing reservation rather than holding stock twice. The key derives from the order, never from the run. A run derived key is indistinguishable from no key at all, because every retry invents a fresh one. I measured that exact failure in an earlier sample, and it produced two authorizations against a balance that only had room for one.

Put the idempotency key in the action layer regardless of which substrate you pick. It is the one control that survives the architecture decision.

What the durable orchestration looks like

The reason this is worth showing rather than describing is that the code contains almost no plumbing. Here is the middle of the fulfillment orchestration, trimmed for length.

[Function(nameof(FulfilOrder))]
public static async Task<FulfilmentOutcome> FulfilOrder(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var request = context.GetInput<FulfilOrderRequest>();
DurableAIAgent agent = context.GetAgent(AgentName);
AgentSession session = await agent.CreateSessionAsync();
// Facts come from activities, never from the model.
var order = await context.CallActivityAsync<Order?>(
nameof(ToolActivities.LookupOrder), request.OrderId);
if (order is null) { /* unknown order, return */ }
// The key derives from the order, never from the run. And there is no stock
// pre-check: reserve_stock IS the stock check, enforced atomically by the
// action layer, which is the only answer that cannot go stale between
// checking and reserving.
var reservationKey = $"resv-{request.OrderId}";
var reservation = await context.CallActivityAsync<ReservationResult>(
nameof(ToolActivities.ReserveStock),
new ReserveArgs(order.Sku, order.Quantity, reservationKey));
if (reservation.Reservation is null) { /* notify once with the action layer's reason, return */ }
// Only now does the model get a turn: book the slot the orchestrator computed,
// and write the customer message. Checkpointed: on replay this returns from
// history without calling the model.
AgentResponse<FulfilmentPlan> plan = await agent.RunAsync<FulfilmentPlan>(
message: $"Stock is held for {request.OrderId}. Call arrange_delivery with " +
$"the slot exactly as '{slot}'. Then write one short message " +
"telling the customer what happens next.",
session: session);
// ...
}

Count what is absent. There is no thread table, no bookkeeping to work out which steps already ran, no resume logic, and no retry loop. context.GetAgent pulls in an agent registered at startup, each RunAsync is a checkpoint, and CallActivityAsync marks the operations that must commit exactly once.

When the model judged a fact

An earlier revision opened this orchestration with a triage turn: the agent was told to look the order up, check inventory, and decide whether there was enough free stock. It worked all day, and then, with twelve units free and two wanted, it returned canFulfil false with the reason “Free stock is lower than the order quantity”. The tools had been called, and the inventory tool had answered twelve. Yet the model fumbled a comparison a CPU does perfectly, the orchestrator took its word for it, and a customer was told an order could not be fulfilled while eleven spare units sat on the shelf. The division of labour above is the correction: activities fetch facts, the action layer enforces constraints, and the model plans and writes, which is the work only a model can do. It never decides whether.

Registration is equally short. In Program.cs you build an ordinary Agent Framework agent against the Foundry project, then hand it to the durable host.

AIAgent agent = new AIProjectClient(new Uri(foundryEndpoint), new DefaultAzureCredential())
.AsAIAgent(model: modelDeployment, instructions: Instructions, name: AgentName,
tools: [ /* the read oriented tools only */ ]);
builder.ConfigureDurableAgents(options => options.AddAIAgent(agent));

Compare that against what the same behavior cost in 2025, and you can see why the received pattern needs updating rather than repeating.

What deploying it actually cost

I would not have found any of the following by reading documentation, and three of them would sink a production build.

The preview and stable halves of one product are on different trains. Microsoft.Agents.AI is stable at 1.19. Microsoft.Agents.AI.DurableTask and Microsoft.Agents.AI.Hosting.AzureFunctions are preview at 1.16. The durable packages hook into internals of the core package, nothing declares an upper bound, and NuGet pairs them without a word. It restores, compiles, deploys and starts. The agent answers in eighteen seconds. Then the orchestrator fails a determinism check, gets retried forever, and reports Running until you go looking. No compiler error, no restore warning, no startup error.

The fix is to pin the whole family to the older train, which means pinning the stable package down to match the previews. That is worth saying out loud in a review, because it is backwards from how anyone manages versions.

Failures that look like working systems

Two Azure Functions apps must not share a deployment container. Flex Consumption keeps the deployed package in the container you name. Point two apps at the same one and the second deployment overwrites the first, so an app runs the other app’s code. It does not fail. The host starts cleanly, registers whatever it found, and every route you wrote returns 404. The only reliable tell is comparing the registered function list against what you wrote.

A client that turns failures into nulls defeats the whole point of a durable activity. My first tool client returned null on any non-success status. When a function key went missing, the agent received “no such order” and reasoned confidently about a fabricated premise, while the notification activity returned success having done nothing. Exactly-once is only a guarantee if a failed call is allowed to look like one. An activity that swallows a transport error is worse than one that crashes, because neither the runtime’s retry nor the orchestration’s error handling ever engages.

Two smaller ones worth carrying. In-process state in the action layer is simply wrong on a platform that scales out, and moving reservations to Table Storage made the idempotency key stronger than the lock it replaced, because a duplicate row key returns 409 atomically across every instance. And the delivery date belongs in the orchestrator, not the prompt: an orchestrator must be deterministic, so time comes from the replay-safe clock. Ask a model for “the earliest weekday morning slot” and you will get a booking reference containing the literal phrase, which is funny once and then a determinism bug forever.

Where the platform enforces the Foundry Agent Service architecture boundary

One thing I expected to argue for turned out not to need arguing. Flex Consumption allows exactly one site per plan, so the action layer and the orchestrator cannot share one. The received diagram calls the action layer “independently scalable” as a design aspiration you could honour or skip. On this platform you have no choice: the two sides of the boundary scale on separate plans by construction and cannot starve each other of instances.

That is a better argument for drawing the line than any of mine, because it is a constraint rather than an opinion.

The four questions that settle it

When teams argue about this, they argue about product features. The argument ends faster if you ask four operational questions instead.

Who owns retry and failure handling when a tool call dies? With a prompt agent, Foundry does. With a durable agent, you do, and you decide the policy. And finally, with a hosted agent, it splits, and the split is the thing to write down.

Where does session state live, and in whose subscription? All three substrates support bring your own resources for conversation state, using Azure Storage, Cosmos DB, or Azure AI Search inside your own subscription and under your own retention policy. This matters enormously for data residency, and it is also why residency alone cannot pick a substrate for you. It used to be the deciding argument. It is now available on every path.

Which control plane does the on call engineer open at three in the morning? This question ends more debates than the other three combined. If the answer is “it depends which agent”, you have not drawn a boundary, you have accumulated one.

Is the topology documented, or improvised per team? Teams that struggle operationally are rarely the ones that picked the wrong substrate. They are the ones that picked different substrates for different agents without recording why, and inherited the failure modes of both with the runbook of neither.

Where the Foundry Agent Service architecture boundary is the wrong frame

I have made this sound tidier than it is. Several situations break the framing above.

When the orchestration is genuinely trivial. A single agent, three tools, no branching, no long running steps. Reaching for a durable orchestrator here buys you preview packages, a Durable Task backend, and a deployment pipeline in exchange for checkpointing you will never exercise. Use a prompt agent and move on.

When your team has no .NET or Python depth. The durable path is code. It rewards a team that already runs Functions, already has infrastructure as code, and already reviews pull requests. It punishes a team that wanted an agent and got a distributed systems project.

Constraints that make the decision for you

When the preview status is disqualifying. Some regulated programs cannot take a dependency on packages with no stable release, whatever the docs imply. That constraint is legitimate and it makes the decision for you. Prompt agents and hosted agents are generally available. Say so in the review and let the constraint do its work.

Do not soften this one. The train skew above is exactly the class of failure that preview status is warning you about, and it cost most of a day to find in a sample with four files of orchestration.

When you actually need multi agent collaboration across organizational boundaries. The Agent2Agent protocol appears in a lot of diagrams, including the one at the top of this post, positioned as a settled capability alongside thread state and tool coordination. It is still preview. Design for it if you like, but do not commit a program to it this quarter.

When you are migrating rather than starting. If you are on Agents classic, the deprecation deadline is doing your planning for you. Classic retires on 31 March 2027, and the migration guide makes clear it is not a rename. That is not far away for an enterprise program with a change freeze and a release train.

Draw the Foundry Agent Service architecture boundary before anyone writes code

The original pattern got the important thing right. There is a boundary, it matters, and architectures fail when nobody draws it. What it got wrong was assuming the Foundry Agent Service architecture boundary always falls in the same place.

It does not. You now have three legitimate substrates above the line and one action layer below it. The action layer stays where it is, and the tools you write for it survive every one of these decisions, which is a good reason to build that layer first and pick the substrate second.

Pick one orchestration substrate per workload. Write down which one and why. Then let the four questions above, rather than a diagram, decide the next one.

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

Azure Functions AI Integration: The Quiet Powerhouse

When people talk about running AI workloads on Azure, the conversation usually lands on Azure AI Foundry, Azure OpenAI Service, or maybe Azure Container Apps. Azure Functions tends to get mentioned as the glue the thing you bolt on to handle a webhook. But this overlooks the real story: Azure Functions AI integration has quietly evolved from simple glue into a powerhouse for running production-grade AI.

That framing is outdated. Azure Functions is now a first-class runtime for AI workloads, with four distinct patterns that the Microsoft Learn documentation lays out explicitly. This post walks through each of them and helps you decide which one fits your situation.

The four AI-enabled scenarios

Microsoft groups Azure Functions AI integration into four scenarios:

  1. Serverless agents runtime — event-driven agents that run on serverless infrastructure
  2. Tools and MCP servers — hosting remote Model Context Protocol servers and AI tools
  3. Agentic workflows — multistep, long-running directed agent operations via Durable Functions
  4. Retrieval-augmented generation (RAG) — fast, parallel data retrieval for knowledge-augmented AI

These are not just marketing buckets. Each one reflects a different architectural decision. Let me unpack them.

Azure Functions AI integration: Serverless agents runtime

The serverless agents runtime is a preview programming model for building event-driven agents as function apps. Moreover, Agents are defined in .agent.md files, app-wide runtime defaults live in agents.config.yaml, and remote MCP server connections are listed in mcp.json. The runtime discovers these files, registers the required triggers and endpoints, and runs the agent through the Microsoft Agent Framework when an event fires.

That is a meaningfully different model from what you get in Azure AI Foundry Agent Service. In Foundry, the managed service hosts and orchestrates your agents. In addition, in the serverless agents runtime, your function app is the agent host running on Flex Consumption, with built-in managed identity, monitoring, and scale-to-zero. Furthermore, you write custom Python tools for app-specific logic, and the platform wires in MCP-enabled connections based on Azure connectors and remote MCP servers.

Use this when: you want agents triggered by events, schedules, messages, or HTTP requests and you need the familiar Functions deployment and hosting model rather than a managed agent service.

Avoid it when: you need a fully managed, enterprise-grade agent service with built-in tooling and long-term Microsoft support guarantees today. That is what Foundry Agent Service is built for.

Azure Functions AI integration: Tools and MCP servers

The Model Context Protocol (MCP) has become the industry standard for how AI models and agents interact with external systems. Azure Functions has first-class support for hosting remote MCP servers, and this is already generally available.

There are two hosting options:

OptionStatusHow it works
MCP binding extensionGAUses Functions triggers and bindings; supports stateful execution
Self-hosted MCP servers (MCP SDK)PreviewUses standard MCP SDKs via custom handlers; requires Streamable HTTP transport

The binding extension is the right default. It supports C#, Python, TypeScript, JavaScript, and Java, and it integrates with the Functions programming model you already know. Self-hosted MCP servers offer portability: you can use the official MCP SDKs and bring in existing server code. However, stateful execution is not yet supported, and the configuration is still changing during preview.

There is also a third option worth knowing about: queue-based Azure Functions tools, where AI agents interact with your code through message queues rather than direct MCP calls. Microsoft Foundry provides specific Azure Functions tooling for this pattern. It is ideal when you need reliable delivery, built-in retry, and decoupling between agent and function execution.

Use MCP servers when: you are exposing tools to AI clients and you want the industry-standard protocol with serverless hosting.

Use queue-based tools when: you need asynchronous, fault-tolerant communication between an agent and your function code.

Agentic workflows with Durable Functions

Not all AI orchestration should be autonomous. Some scenarios need predictable, directed steps and that is where Durable Functions fits.

The Microsoft Learn documentation makes the distinction clearly: Durable Functions is positioned as the runtime for directed agentic workflows, not for emergent agent reasoning. Think of it this way: when you know the sequence of steps and you need fault tolerance, auditability, and long-running execution, Durable Functions is the right tool. When you want a model to figure out the steps dynamically, you want an agent runtime.

The documentation gives a clean example: a trip planning workflow that gathers user requirements, searches for options, waits for approval, and makes bookings. Each step is a function; Durable Functions coordinates them with built-in retry, state persistence, and human-in-the-loop support.

Use this when: your AI-driven process has well-defined, ordered steps,authorization flows, multi-stage approval chains, or orchestrated data pipelines where you cannot afford unpredictable execution paths.

Avoid it when: you want a model to determine steps dynamically. That is the serverless agents runtime or Foundry Agent Service territory.

RAG with Azure Functions

Because Functions handles multiple events from various data sources simultaneously, it scales well for real-time AI scenarios, particularly RAG systems where fast, parallel retrieval is the bottleneck.

The Azure OpenAI binding extension lets you integrate RAG directly into your function code. Functions can pull data from multiple sources simultaneously, feed it through Azure AI Search or other retrieval layers, and pass the results to your language model, all within the event-driven, scale-to-zero model that keeps costs down when load is low.

The Azure Functions RAG pattern also pairs naturally with APIM, which handles routing, rate limiting, and token quota management a pattern the Citadel Platform series covers in detail, including the discovery that the Foundry Agent Service SDK bypasses APIM for LLM calls.

Use this when you have event-driven retrieval requirements new documents arriving in blob storage, database change feeds, or streaming IoT data that needs to inform model responses.

How the scenarios relate to other Azure services

It helps to think of Azure Functions AI integration as filling the compute and integration layer between your AI services and your data sources. Here is roughly how that maps:

  • Azure AI Foundry Agent Service — fully managed agent orchestration with enterprise security and built-in tools. Functions integrates into Foundry via MCP servers and queue-based tools.
  • Azure Logic Apps — low-code orchestration for business process automation. Functions is the right choice when you need custom code, complex event processing, or lower latency.
  • Azure Container Apps — container-based hosting for long-running services. Functions on Flex Consumption beats it on cost for bursty, event-driven AI workloads that spend time idle.
  • Durable Functions — lives inside Functions and adds stateful, long-running orchestration. Use it for directed agentic workflows; use the serverless agents runtime for event-driven agents.

The underlying platform advantage

Across all four scenarios, the same hosting model applies: Flex Consumption. It offers fast, event-driven scaling, virtual network integration, and pay-as-you-go billing. For AI workloads, which tend to be bursty rather than continuous, this is a significant cost advantage over always-on hosting.

Managed identity, Application Insights integration, and azd-based deployment are consistent across all four patterns. That means your security posture, observability, and deployment pipeline do not have to change when you move from a simple timer trigger to hosting a remote MCP server.

Azure Functions AI integration: Choosing the right pattern

Here is a simple decision table:

I want to…Use…
Build event- or schedule-triggered agents with MCP toolsServerless agents runtime (preview)
Expose tools to AI clients via the industry-standard protocolMCP binding extension (GA)
Orchestrate predictable, multistep AI-driven processesDurable Functions
Build a RAG pipeline with fast, parallel data retrievalAzure Functions + Azure OpenAI binding
Fully managed agent hosting with enterprise SLAsAzure AI Foundry Agent Service

What comes next

The rest of this series goes deep on each pattern. The next post covers the two MCP server hosting options in detail binding extension versus self-hosted SDK servers, including where the current preview constraints matter in practice.

Up next: Hosting Remote MCP Servers in Azure Functions: GA vs. Preview Options

Building Azure Logic Apps Agent Tools: Connectors and MCP

Part 4 of 7 in the Logic Apps Agent Loop series

Part 3 covered the two agentic workflow patterns in Azure Logic Apps, autonomous and conversational, and how to choose between them. Both patterns rely on the same mechanism for getting work done: tools. An Azure Logic Apps agent loop tool is the means by which the model reaches out to the world to query a database, send an email, call an API, or retrieve a document. Without tools, the agent can only reason over what the model already knows.

This post is the most hands-on in the series. It covers the three layers of the Azure Logic Apps tooling model, built-in connectors, custom connectors, and MCP servers. Moreover, it includes a demo showing how to expose a Logic Apps workflow as a tool provider that can be called by an external agent in Azure AI Foundry.

Choosing the right Azure Logic Apps agent tools layer

Before building anything, it is important first to understand what a tool actually is in Logic Apps terms. Specifically, a tool is defined as a sequence of one or more connector actions that the agent can choose to invoke during a loop iteration. Consequently, the model decides which tool to call based on the tool’s name and description. Therefore, naming and describing tools clearly is one of the most crucial decisions you will make when building an agentic workflow.

Logic Apps offers three layers of tooling, each adding capability and complexity.

Layer 1: Built-in and managed connectors

The foundation layer is the 1,400+ connector library that Logic Apps has always offered. For agent tools, the most relevant connectors are those that give the agent access to data and services: Azure OpenAI, Azure AI Search, Azure Blob Storage, Office 365 Outlook, SharePoint, SQL Server, HTTP, and Service Bus among them.

You build a tool by adding one or more of these connector actions inside the tool container within the agent action. Each tool gets a name and a description. The model reads these at runtime to decide whether to invoke the tool and what arguments to pass. You then create agent parameters for any action inputs that the model should supply dynamically: a city name for a weather lookup, a query string for a search, a recipient address for an email.

Agent parameters differ from standard Logic Apps parameters importantly. They are scoped to the tool where you define them; they cannot be shared across tools. They also receive their values only when the agent invokes the tool, not at workflow start time. You can call the same tool multiple times in a single loop using different parameter values: for example, you could invoke a weather tool for both Amsterdam and London in the same run.

Layer 2: Custom connectors

Where the built-in connector library has gaps, custom connectors fill them. A custom connector in Logic Apps is an OpenAPI-described wrapper around any REST API, internal or external. Furthermore, once you register it, it appears in the connector gallery just like a managed connector, and you can use it inside a tool in the same way.

For enterprise integration architects, custom connectors are the bridge between the agent loop and any internal system that does not have a first-party Logic Apps connector: an internal HR system, a legacy claims processing API, a proprietary data platform. The investment in defining the OpenAPI specification pays off because the connector becomes reusable across all workflows in the tenant, not just the agentic ones.

Building a custom connector for use in an agent tool follows the standard Logic Apps custom connector creation process:: define the API, specify authentication, and configure the operations, with one addition: write clear operation descriptions, because the model uses these descriptions to decide when to invoke the connector.

Layer 3: MCP servers

The third layer is the newest and the most architecturally significant. Azure Logic Apps can serve as the backend for a Model Context Protocol (MCP) server exposing connector actions as a structured, discoverable toolset that external agents and models can call over a standard protocol.

MCP is an open standard that defines how AI components discover and invoke tools. Moreover, an MCP server acts as a bridge between an AI agent and the tools it can use. This is a significant shift from the previous two layers. Built-in and custom connectors are tools that the agent in your Logic Apps workflow invokes. An MCP server inverts the relationship: your Logic Apps workflow becomes the tool provider, and the calling agent lives somewhere else entirely.

A note on the demo: real-world limitations of the tooling preview

For this post I set out to build a working end-to-end demo showing a Logic Apps workflow exposed as an MCP tool provider callable by an Azure AI Foundry agent. The concept is sound and the architecture is correct, but two practical blockers prevented a clean demo at the time of writing.

API Center MCP wizard limitations. The registration wizard in Azure API Center is in active preview. The connector picker surfaces only managed connectors, so the built-in HTTP action from Part 2 is unavailable. The logic app dropdown is also filtered by region, a logic app in West Europe will not appear in an API Center resource deployed to a different region.

Foundry OpenAPI tool network restrictions. Azure AI Foundry’s OpenAPI tool sandbox cannot reach azurewebsites.net endpoints directly. Calls from the Foundry playground return an Unknown error regardless of the spec configuration. The workaround is to front the Logic Apps endpoint with Azure API Management, which Foundry can reach however that adds infrastructure complexity beyond the scope of this post.

Both limitations are preview-stage issues that Microsoft will likely resolve. The OpenAPI spec, the Foundry agent configuration, and the mcp-research workflow pattern described above are all correct and will work once network access between Foundry and Logic Apps endpoints is available or via an APIM gateway.

The Layer 3 pattern of your Logic App as a tool provider for any external MCP-compatible agent remains the most architecturally significant development in this series. In addition, Part 6 picks up the security implications of that expanded caller surface.

Choosing the right tooling layer

The table below summarises how Azure Logic Apps agentic workflows differ across the three tooling layers.

Built-in connectorsCustom connectorsMCP server
Who calls the toolAgent in your workflowAgent in your workflowAny external MCP-compatible agent
Setup complexityLowMediumMedium–high
ReusabilityWithin the workflowAcross the tenantAcross agents and platforms
Best forStandard integrationsInternal APIs without a connectorMulti-agent, cross-platform tooling

The three layers are not mutually exclusive. A production agentic workflow will typically use built-in connectors for standard integrations, custom connectors for internal systems, and an MCP server where the toolset needs to be shared across multiple agents or platforms.


What comes next

The next post moves from individual tools to multi-agent composition. Part 5 covers orchestrator-worker topologies, agent handoffs, and how to build sequential agent loops.

Azure API Management as MCP Gateway: Governing Agentic AI Workloads

Part 7 of 7 in the “APIM for AI Workloads” series

Azure API Management as MCP gateway is the natural endpoint of everything this series has built. In Parts 1 through 6, we established APIM as the control plane for AI workloads: securing access, limiting and measuring token consumption, routing traffic resiliently across backends, and reducing costs through semantic caching. All of that applies equally to agentic workloads. The difference is that agents introduce a new communication pattern: the Model Context Protocol (MCP), which standardizes how AI agents discover and call tools.

In my work and online research on agentic AI architecture, I consistently returned to the same question: how does one govern agent tool calls with the same rigor we apply to API calls? The answer, increasingly, is that APIM handles both. This post covers what that looks like in practice.

What MCP Is and Why It Changes the APIM Story

MCP is an open protocol, originally developed by Anthropic, that defines a standard interface between AI agents (MCP clients) and the tools they call (MCP servers). Instead of each agent framework implementing its own bespoke tool-calling mechanism, MCP gives agents a consistent way to discover available tools, understand their input schemas, and invoke them. Frameworks including Semantic Kernel, AutoGen, and LangGraph are all adding MCP client support.

For APIM, MCP matters because it transforms the gateway from a proxy for AI completions into a broker for agent tool calls. An agent no longer calls your internal APIs directly. Instead, it discovers them as MCP tools through APIM, and APIM enforces the same governance policies on those tool calls that it enforces on any other request. The control plane extends naturally into the agentic layer.

Azure API Management as MCP Gateway: Three Capabilities

APIM’s MCP gateway capabilities fall into three categories:

Expose REST APIs as MCP servers. The export-rest-mcp-server policy takes any API already registered in your APIM catalog and auto-generates MCP tool definitions from it. An agent connecting to your APIM MCP endpoint discovers those tools via the standard MCP protocol and can call them without any knowledge of the underlying REST implementation. Crucially, no changes are required to the underlying API. The policy handles the translation layer entirely within APIM.

Pass through external MCP servers. APIM can proxy external MCP servers — whether third-party services like GitHub or Jira, or custom MCP servers built by your own teams — through the same gateway. All traffic passes through APIM’s policy pipeline, so you apply JWT validation, subscription key enforcement, token limits, and logging to external MCP calls exactly as you would to any other API call. Agents get a single APIM endpoint; APIM handles the routing.

Agent-to-agent (A2A) traffic. In multi-agent architectures, orchestrator agents call sub-agents to delegate tasks. Routing that traffic through APIM means every A2A hop is governed: authenticated, rate-limited, logged, and subject to the same token budget controls applied to end-user traffic. This is particularly relevant for agentic pipelines running on Microsoft Foundry, where multiple specialized agents collaborate within a single workflow.

Applying Series Policies to Agentic Workloads

One of the practical advantages of routing MCP traffic through APIM is that every policy covered in this series applies without modification. Agentic workloads are not a special case requiring a separate governance layer. They use the same pipeline.

  • Authentication (Part 2): Agents authenticate to APIM using subscription keys or JWT tokens. APIM authenticates to AI backends via Managed Identity. The agent never holds backend credentials.
  • Token limits (Part 3): Multi-step agentic pipelines can consume large token volumes per workflow. Per-subscription TPM limits prevent a single runaway pipeline from exhausting shared capacity.
  • Token metrics (Part 4): Token consumption from agentic workflows is attributed to the subscribing team or pipeline via the emit-token-metric policy. FinOps visibility extends automatically to agentic workloads.
  • Load balancing (Part 5): Agentic pipelines often run longer and consume more tokens per call than chat applications. PTU-to-PAYG failover protects pipeline continuity when primary capacity saturates.
  • Semantic caching (Part 6): Agents that make repeated identical tool calls, checking a status, or looking up a reference value, benefit from semantic caching in the same way chat applications do.

Practical Considerations for APIM as MCP Gateway

A few agentic-specific considerations are worth calling out before you start routing MCP traffic through APIM.

Tool discovery latency. MCP clients typically discover available tools at session start by calling the MCP server’s tool list endpoint. With APIM in the path, that discovery call passes through the full policy pipeline. Keep your inbound policies lightweight for discovery calls, or cache the tool list response to avoid repeated round trips.

Streaming responses. Many AI completions endpoints support streaming via server-sent events. APIM supports streaming passthrough, but some policies — including semantic cache lookup — do not apply to streaming responses. Structure your pipeline accordingly: apply caching only to non-streaming completion calls.

Session state. MCP conversations are stateful within a session. APIM is stateless between requests, so per-session state must live in the calling agent or an external store. The vary-by pattern from the semantic cache policy can scope cached tool responses by session ID if the agent passes one in a header.

Token budget propagation. In multi-agent pipelines, token budgets need to propagate from the orchestrator to sub-agents. Exposing the remaining token budget from the remaining-tokens-variable-name attribute (Part 3) as a response header lets orchestration frameworks like Semantic Kernel make informed decisions about which sub-agent to invoke next.

Azure API Management as MCP Gateway: Closing the Series

This post closes the series, but the control plane it describes is not static. MCP is still evolving rapidly. New APIM policy capabilities for agentic workloads are shipping frequently. The architecture board conversation at various enterprise has shifted from “should we centralize AI traffic through APIM?” to “what do we govern next?”, which is a good place to be.

Looking back across the seven posts, the consistent theme is that AI workloads are not fundamentally different from other API workloads in terms of governance requirements. They need authentication, rate limiting, observability, resilience, and cost control. APIM provides all of those. What changes with AI is the unit of measurement (tokens, not requests), the billing model (PTU vs. PAYG), and now the communication protocol (MCP for agents). The control plane adapts to each of these without requiring a parallel governance infrastructure.

The full series index is below for reference. Each post links to the relevant Microsoft documentation and includes policy XML you can use directly.

  • Part 1: Why your AI APIs need a gateway.
  • Part 2: Authentication and authorization.
  • Part 3: Token limit policy.
  • Part 4: Token metric policy and cross-charging.
  • Part 5: Load balancing and circuit breaking.
  • Part 6: Semantic caching.

Part 7 (this post): APIM as MCP gateway for agentic AI workloads.