A practical, end-to-end walkthrough of taking a read-only slice of an enterprise source system and exposing it to an AI agent as a set of Model Context Protocol (MCP) tools — using Azure Logic Apps, API Management, and Agent Foundry. All identifiers below are placeholders; swap in your own.
We wanted a simple outcome: a user asks a business question in plain language and gets a straight answer — no dashboards, no field names, no training on the underlying system. That means an AI agent with safe, read-only access to a backend system of record, exposed as discrete tools the model can call.
The constraints shaped every decision:
Five layers, one direction of flow:
Agent (Agent Foundry)
│ MCP (JSON-RPC over HTTP)
▼
MCP server (API Management)
│ REST operation per tool
▼
API gateway (API Management)
│ single POST, routed by body
▼
Tool executor (Logic App)
│ OAuth token + REST calls
▼
Source system (REST API)
The key design choice: one backend endpoint, many logical tools. The Logic App exposes a single HTTP trigger that accepts { "tool": "records.list", "parameters": { ... } }
and routes internally. API Management then fans that single endpoint out into many named operations, and its MCP feature turns those operations into agent tools. This keeps the backend trivial to maintain while the agent still sees a rich, typed tool catalog.
Start from the questions, not the schema. A useful toolset usually falls into a few families:
Two implementation notes that matter a lot:
Prefer aggregates over "pull a list and count." If the source system can compute a sum or a group-by server-side, a "total by category" answer is one summary row, not hundreds of records the model has to tally. Push the work down to the system wherever you can.
Prototype against the real API first. A quick collection of calls against the source system's REST endpoint (with OAuth client-credentials) lets you nail the field names, filters, and relationships before you touch orchestration. Field-name mismatches are the most common silent failure — verify against real responses.
The Logic App is a dispatcher. One HTTP request trigger, then:
first(split(tool, '.'))
).Switch
on category, with a nested Switch
on the full tool id inside each — each leaf is one read-only REST call.A few hard-won details:
@
.@
alias, remember that Logic Apps reads a bare @
as an expression. Double it (@@
) so it renders as a literal @
at runtime.{ "error": "unknown tool" }
object; each matched case overwrites it. Unknown tools then return cleanly instead of hanging.The agent needs a tool schema per operation. Generate an OpenAPI document with one operation per tool, where each operation's request body is only that tool's parameters (a get
op takes a record id; an aggregate takes nothing). Keep the path equal to the tool id so a gateway policy can recover it later.
Generate this programmatically from the same source of truth as the Logic App so the two can't drift — derive each operation's parameters from the inputs the Logic App actually reads.
Gotcha:API Management's inline "OpenAPI specification" editor parses the document asSwagger 2.0. Paste an OpenAPI 3.0 doc and you get"The Swagger version specified is unknown."Either import via theAdd API → OpenAPIsurface (which accepts 3.0), or hand it a Swagger 2.0 document. Same operations either way.
Import the OpenAPI to create the operations, then add one API-scoped inbound policy that:
validate-jwt
with your audience),{ tool, parameters }
body the Logic App expects,
<set-variable name="toolId" value="@(context.Request.OriginalUrl.Path.Split('/').Last())" />
<set-body><![CDATA[@{
var incoming = context.Request.Body?.As<JObject>(preserveContent: true) ?? new JObject();
if (incoming["tool"] != null) { return incoming.ToString(); } // already wrapped
var wrapped = new JObject();
wrapped["tool"] = (string)context.Variables["toolId"];
wrapped["parameters"] = incoming;
return wrapped.ToString();
}]]></set-body>
Authenticating to the backend — drop the shared secret. A Logic App request trigger defaults to a SAS signature (sig
) baked into its URL. That's a shared secret you then have to store and rotate. The better pattern: give API Management a managed identity, add an OAuth authorization policy on the Logic App trigger (issuer + audience, optionally locking to APIM's appid
), disable SAS on the trigger, and let APIM authenticate with its own identity:
<authentication-managed-identity resource="api://<APP_ID>" />
No sig
, nothing to rotate, and you can restrict the trigger to exactly one identity.
API Management can publish selected operations as an MCP server. Point its "expose an API as an MCP server" feature at your API, select the operations you want as tools, and it hands you a server URL (an /mcp
endpoint). The operation operationId
s become the tool names the agent sees, so keep them clean (records_list
, analytics_by_category
).
Tier caveat:MCP server export isn't available on the Consumption tier of API Management. If the MCP Servers blade is missing, that's why.
In the agent, add a Model Context Protocol tool:
aud
your gateway's validate-jwt
requires. The audience is the single most common misconfiguration — get it right and the token check passes.If the gateway 401s and the token looks right, paste it into a JWT decoder and check iss
/aud
against the policy. Managed-identity tokens use the v1 issuer (https://sts.windows.net/<tenant>/
); configuring the policy with the v2 issuer produces a mismatch.
Tools enforce read-only at the API layer, but the behavior — tone, safety, honesty — lives in the system prompt. Ours does three jobs:
That last point is worth underlining: the agent should never hand-craft queries or field names. It picks a tool and fills named inputs. If a question needs a field the tools don't expose, that's a capability gap to report, not something to invent.
The demo worked on the first real question. The next few turns taught us the operational lessons:
The result is an agent a non-technical user can talk to in plain language, backed by a boringly maintainable pipeline, with the secret-handling and injection surface handled deliberately rather than by accident.
— Engineering notes, generalized. Replace all <placeholders> with your own tenant and resource values.