There is one question that dismantles most “chat with your data” demos: whose permissions does the agent query with?
The usual answer is uncomfortable. The agent connects to the data store with a service identity — a service principal, a managed identity, an API key — that can read everything. The user asks, the agent queries as a superuser, and row-level security (RLS), workspace permissions and the entire governance model that took years to build evaporate at the first hop.
As long as that holds, the agent never leaves the demo. No serious data owner rolls out a system where asking a question means bypassing permissions.
This article describes how we solved that problem for a Spanish energy company: a production data agent where the question travels with the identity of the person asking it — from Teams, Claude or VSCode all the way to Microsoft Fabric — and RLS applies exactly as if the user had queried by hand.
Before the how, the result: an analyst asking in production — with their own identity — and the answer in seconds, with the tables consulted at the foot.

The pattern: one MCP server, three clients, one identity
The architecture has three planes:
- Access channels — where users already work: M365 Copilot (in Teams, via a declarative agent), Claude (Claude Code and claude.ai) and VSCode. Three clients, zero new applications to learn.
- MCP server — a single remote server (FastAPI on Azure Container Apps) exposing the agent’s tools over HTTP using the Model Context Protocol. Every channel talks to the same server.
- Data plane — the corporate lakehouse on Microsoft Fabric: OneLake for Delta metadata and the SQL Analytics Endpoint for queries.

The important structural decision is not any box in the diagram — it’s the arrow. Every request reaching the MCP server carries the OAuth token of the user asking, and the server never swaps it for an identity of its own.
The OBO chain, step by step
The mechanism is OAuth 2.0 On-Behalf-Of (OBO), a standard Entra ID flow designed for exactly this: a middle-tier service that needs to call downstream APIs on behalf of the user, not on its own behalf.

- Login. The user authenticates with their corporate Microsoft account — Teams SSO, the MCP client’s login, or
az login. No new passwords, no parallel accounts. - Inbound token. The client calls the MCP server with a token whose audience is the server itself. The server validates signature, audience and issuer.
- OBO exchange. The server presents that token to Entra ID with the
urn:ietf:params:oauth:grant-type:jwt-bearergrant and requests tokens for the data APIs. Entra ID issues new tokens that still carry the original user’s identity. - Data tokens. Two audiences:
https://storage.azure.com/.default(OneLake, Delta metadata reads) andhttps://database.windows.net/.default(SQL Analytics Endpoint, via pyodbc/ODBC 18). - Query. Fabric receives the query as if the user had issued it. RLS, workspace and lakehouse permissions apply without the agent having to reimplement anything.
The server’s app registration needs three delegated permissions with admin consent — and only three:
Microsoft Graph → User.Read (profile after login)
Azure Storage → user_impersonation (OBO towards OneLake)
Azure SQL Database → user_impersonation (OBO towards the SQL endpoint) And the anti-pattern is forbidden by design: the container’s Managed Identity exists for infrastructure only (pulling the image, reading secrets, writing telemetry). If anyone proposes “just give the MI Storage Blob Data Reader on the lakehouse and simplify”, the answer is no — that is precisely the hole RLS escapes through. Any such proposal gets redesigned with OBO.
Three ecosystems, the same contract
What makes MCP interesting as an integration layer is that the identity work is done once and every client inherits it:
- Claude Code / VSCode connect over remote HTTP MCP with OAuth — the user’s token travels with every call. The VSCode extension reuses the same configuration (
.mcp.json); zero extra code. - M365 Copilot consumes the same server through a declarative agent — Microsoft’s current format for agents in the Teams catalog (the Bot Framework path for this use case is archived). The manifest declares the MCP plugin and the OAuth proxy; identity arrives all the same.
The declarative agent manifest, schematically:
{
"version": "v1.2",
"name": "Data agent",
"instructions": "You answer questions about corporate data…",
"actions": [
{
"id": "mcp-plugin",
"file": "mcp-plugin.json"
}
]
} (Schema of declarativeAgent.json, no real identifiers; mcp-plugin.json points to the remote MCP server and declares the OAuth proxy.)One practical consequence of sharing the server: response latency differs by channel — in Copilot inside Teams answers arrive in minutes; in Claude, in seconds (as in the capture at the top). The identity chain is identical in both.
Design decisions that matter
Read-only in code, not in policy. The server accepts exclusively SELECT/WITH/DESCRIBE/SHOW; any other statement is rejected before it touches the endpoint. An agent that can write is a different conversation — this architecture closes it off at the root.
Audit per query. Every call records who asked, what they queried and when. With OBO this is free: the identity already comes with the request; nothing has to be reconstructed.
Permission errors as signal, not failure. When Fabric returns a 403, the agent tells the user plainly (“you don’t have access to that table”) instead of masking it. Silent over-access is the bug; the visible 403 is the feature.

Deliberately boring deployment. Terraform, an image in Azure Container Registry, Container Apps revisions. Deployment runs as a dedicated service principal the client controls and can revoke; admin consent on the delegated permissions remains a deliberately manual step on the client’s side — it is the boundary between “we trust the app” and “the provider can run Terraform”.
The knowledge layer: versioned skills
An agent that merely translates natural language to SQL gets the part that matters wrong: the conventions. Which table is canonical, what each code means, which units the measurements come in, which filters apply by default.
That knowledge lives as skills: documents versioned in git, organized into plugins by domain, that the agent consults at runtime. The same repository is distributed to all three ecosystems — as plugins in Claude Code and VSCode, vendored into the server image for Copilot.
The operational effect is what changes maintenance: the agent improves by editing text, not by redeploying code. An analyst fixes a units convention in a markdown file, commits, and all three channels answer better.
The pattern, in three lines
- The user’s identity travels end to end (OBO); the agent never queries as a superuser, and the lakehouse’s RLS and permissions apply intact.
- A single MCP server serves Copilot, Claude and VSCode — the identity work is done once.
- Read-only by design, audit per query, and domain knowledge versioned in git.
If you want the version without the technicalities — what changes for the business when any employee can ask the data in plain language — it’s in From days to minutes: ask your data in plain language.
Got a lakehouse and want an agent that respects it? Let’s talk.
Keep reading
Related articles you might enjoy

From days to minutes: ask your data in plain language
In most companies, answering a business question with data takes days: ticket, analyst, query, Excel. How a Spanish energy company cut it to minutes — any employee asks in their own language, from Teams or Claude, and gets the answer with their own permissions. No new passwords, no bypassing governance.
Read
Besós and San Roque: same owners, opposite outcomes
Two Spanish CCGT complexes share the same Endesa/Naturgy ownership split — and produce opposite results under Operación Reforzada. In Barcelona, Naturgy's unit wins; in Cádiz, Endesa's dominates. The difference is geography: mixed clusters reward the operator, uniform clusters reward the location.
Read
Who pockets the hidden RT3 cost: €3,900M of pay-as-bid concentrated in 5 utilities
Twelve months after the Iberian blackout, the pay-as-bid Spain pays its CCGT fleet for solving day-ahead technical constraints (consumer-side RT3) is €3,870M per year. Three groups absorb 62% of the flow. Iberdrola is the marginal winner (+€152M post vs pre); Endesa, the only major loser (-€100M).
Read