<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DenizV</title>
    <description>The latest articles on DEV Community by DenizV (@denizv).</description>
    <link>https://dev.to/denizv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4034546%2F5912ce4a-6a3f-429a-955c-9a3be1b208dc.png</url>
      <title>DEV Community: DenizV</title>
      <link>https://dev.to/denizv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/denizv"/>
    <language>en</language>
    <item>
      <title>Giving an AI assistant read-only access to Microsoft Loop — without breaking permissions</title>
      <dc:creator>DenizV</dc:creator>
      <pubDate>Sat, 18 Jul 2026 01:03:39 +0000</pubDate>
      <link>https://dev.to/denizv/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking-permissions-3adc</link>
      <guid>https://dev.to/denizv/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking-permissions-3adc</guid>
      <description>&lt;h1&gt;
  
  
  Giving an AI assistant read-only access to Microsoft Loop — without breaking permissions
&lt;/h1&gt;

&lt;p&gt;I wanted my AI assistant to read my team's Microsoft Loop pages — summarize a&lt;br&gt;
workspace, pull the latest OKRs into a draft, answer "what did we decide about&lt;br&gt;
X." Simple ask. It turned into a genuinely interesting engineering problem, and&lt;br&gt;
I ended up building a small open-source MCP server to solve it. Here's the&lt;br&gt;
challenge, the solution, and the traps along the way.&lt;/p&gt;

&lt;p&gt;The result is open source (MIT): &lt;strong&gt;&lt;a href="https://github.com/DenizV/loop-reader-mcp" rel="noopener noreferrer"&gt;loop-reader-mcp&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;Two walls, right at the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wall 1: Microsoft Loop has no content API.&lt;/strong&gt; As of mid-2026, there's no&lt;br&gt;
Graph endpoint to read or write a Loop page's content — no "get page," no&lt;br&gt;
"list workspace." Loop is positioned against Notion and Confluence, but you&lt;br&gt;
can't programmatically get your own content out in a structured way. The 2026&lt;br&gt;
Loop roadmap is about governance, not a content API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wall 2: where Loop actually stores things.&lt;/strong&gt; Loop workspace pages live in&lt;br&gt;
&lt;strong&gt;SharePoint Embedded (SPE)&lt;/strong&gt; containers, and Loop components from Teams/Outlook&lt;br&gt;
live as &lt;code&gt;.loop&lt;/code&gt; files in OneDrive. There &lt;em&gt;is&lt;/em&gt; a documented trick: Microsoft&lt;br&gt;
Graph can convert a &lt;code&gt;.loop&lt;/code&gt; file to HTML on the fly with&lt;br&gt;
&lt;code&gt;GET /drives/{id}/items/{id}/content?format=html&lt;/code&gt;. So reading is possible&lt;br&gt;
through the file layer even without a Loop API.&lt;/p&gt;

&lt;p&gt;But then the real problem showed up — the one worth writing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The permission trap
&lt;/h2&gt;

&lt;p&gt;SharePoint Embedded &lt;strong&gt;does not accept delegated (per-user) tokens&lt;/strong&gt; for content&lt;br&gt;
downloads. Only an &lt;strong&gt;app-only&lt;/strong&gt; identity can fetch the bytes.&lt;/p&gt;

&lt;p&gt;If you build the naive thing — a service that uses its app identity to read&lt;br&gt;
Loop — you've created a permission-flattening machine. The app can read&lt;br&gt;
&lt;em&gt;everything&lt;/em&gt;, so anyone who can talk to your service can read any Loop page in&lt;br&gt;
the tenant, regardless of what they personally have access to. That's a data&lt;br&gt;
leak with extra steps. Unacceptable.&lt;/p&gt;

&lt;p&gt;So the question became: &lt;strong&gt;how do you let a service read content with an app&lt;br&gt;
identity, while guaranteeing each user only sees what they're allowed to see?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: split discovery from retrieval
&lt;/h2&gt;

&lt;p&gt;The insight that cracked it: &lt;strong&gt;Graph Search &lt;em&gt;is&lt;/em&gt; security-trimmed for delegated&lt;br&gt;
callers, even for SPE content.&lt;/strong&gt; Search respects the user's permissions; only&lt;br&gt;
the content &lt;em&gt;download&lt;/em&gt; needs the app identity.&lt;/p&gt;

&lt;p&gt;So I split the two operations across two identities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discovery runs as the user.&lt;/strong&gt; When the assistant searches Loop, the server&lt;br&gt;
exchanges the user's token via the OAuth &lt;strong&gt;On-Behalf-Of (OBO)&lt;/strong&gt; flow and&lt;br&gt;
calls Graph Search &lt;em&gt;as that user&lt;/em&gt;. Microsoft trims the results to exactly&lt;br&gt;
what they can access. The server records the &lt;code&gt;(driveId, itemId)&lt;/code&gt; of every&lt;br&gt;
hit in a short-lived, per-user cache.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieval is gated by that discovery.&lt;/strong&gt; When the assistant asks to read a&lt;br&gt;
page, the server refuses unless that exact &lt;code&gt;(driveId, itemId)&lt;/code&gt; pair is in&lt;br&gt;
&lt;em&gt;this user's&lt;/em&gt; cache — i.e. unless they personally just discovered it via&lt;br&gt;
their own trimmed search. Only then does it use the app identity to fetch&lt;br&gt;
and convert the bytes.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search  -- OBO (user identity) --&amp;gt; Graph Search  -&amp;gt; results trimmed by Microsoft
                                                  -&amp;gt; (driveId,itemId) cached per user
read    -- is this pair in the caller's cache? --&amp;gt; no  -&amp;gt; refuse (no Graph call)
                                                   yes -&amp;gt; app identity -&amp;gt; ?format=html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The authorization decision is Microsoft's, not mine.&lt;/strong&gt; A user can't discover&lt;br&gt;
a page they can't access (search runs as them), and can't read a page they&lt;br&gt;
didn't discover. The app identity is just a retrieval mechanism for bytes the&lt;br&gt;
user already proved they can see. Permission flattening solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it a proper MCP server
&lt;/h2&gt;

&lt;p&gt;I exposed this as a remote &lt;strong&gt;Model Context Protocol&lt;/strong&gt; server with three&lt;br&gt;
read-only tools: &lt;code&gt;loop_search&lt;/code&gt;, &lt;code&gt;loop_list_components&lt;/code&gt;, and &lt;code&gt;loop_get_page&lt;/code&gt;.&lt;br&gt;
Read-only isn't a policy toggle — the Graph client only permits &lt;code&gt;GET&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;POST /search/query&lt;/code&gt;, so there's structurally no way to write. (Good, because&lt;br&gt;
overwriting a &lt;code&gt;.loop&lt;/code&gt; file with anything else corrupts it, and there's no&lt;br&gt;
supported write API anyway.)&lt;/p&gt;

&lt;p&gt;For auth, I initially tried to put a platform "easy auth" gateway in front of&lt;br&gt;
the server. &lt;strong&gt;Big mistake&lt;/strong&gt; — the gateway intercepted the OAuth handshake and&lt;br&gt;
the MCP client could never discover where to log in. The fix was to make the&lt;br&gt;
server its own OAuth resource server: it publishes the discovery documents&lt;br&gt;
(&lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt; and &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;)&lt;br&gt;
pointing clients at Microsoft Entra, and it validates the incoming token itself&lt;br&gt;
(signature via Entra's JWKS, audience, issuer, expiry). No gateway, no&lt;br&gt;
interception, and access is still fully gated because Entra only issues tokens&lt;br&gt;
to users assigned to the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned (the un-glamorous half)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delegated search + app-only retrieval is a legitimate, powerful pattern&lt;/strong&gt;
for any service sitting on top of SharePoint Embedded. Trim on discovery,
fetch on retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let a platform auth gateway wrap a remote MCP server&lt;/strong&gt; — the MCP
OAuth flow needs the client to reach the server's own discovery/challenge.
Let the app own auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind the retrieval gate to &lt;code&gt;(driveId, itemId)&lt;/code&gt; pairs, not just the item ID&lt;/strong&gt;
— otherwise a caller could replay a discovered ID against a different drive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed everywhere&lt;/strong&gt;: no token, no identity, cache miss, expired entry -&amp;gt;
refuse. On multi-instance hosting a user just re-searches; that's a safe
failure, not a leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app credential is the crown jewel.&lt;/strong&gt; It holds tenant-wide read. Store
it in a secrets manager, prefer a certificate, rotate it, and consider
IP-allowlisting the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the revocation lag.&lt;/strong&gt; Discovery results are cached for a few minutes,
so losing access isn't instant. Tune the TTL to your risk tolerance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub under MIT: &lt;strong&gt;&lt;a href="https://github.com/DenizV/loop-reader-mcp" rel="noopener noreferrer"&gt;github.com/DenizV/loop-reader-mcp&lt;/a&gt;&lt;/strong&gt;. The README covers the&lt;br&gt;
Entra app registration, the one-time SharePoint Embedded guest registration,&lt;br&gt;
hosting, and wiring it into an MCP client. &lt;code&gt;SECURITY.md&lt;/code&gt; documents the model and&lt;br&gt;
the residual risks to close before production.&lt;/p&gt;

&lt;p&gt;It's community code, not a certified product — review it and run a dependency&lt;br&gt;
scan before you point it at real data. But if you've been wanting to let an&lt;br&gt;
assistant &lt;em&gt;read&lt;/em&gt; your Loop content without handing it the keys to the whole&lt;br&gt;
tenant, this pattern works.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you build on it or find a sharper approach, I'd love to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>microsoftgraph</category>
      <category>oauth</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
