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

Leave a Reply