{"slug": "giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking", "title": "Giving an AI assistant read-only access to Microsoft Loop — without breaking permissions", "summary": "A developer built an open-source MCP server called loop-reader-mcp to give AI assistants read-only access to Microsoft Loop pages without breaking permissions. The solution splits discovery and retrieval across two identities: user-level OBO tokens for search and app-only identity for content retrieval, gated by a per-user cache to prevent permission flattening.", "body_md": "I wanted my AI assistant to read my team's Microsoft Loop pages — summarize a\n\nworkspace, pull the latest OKRs into a draft, answer \"what did we decide about\n\nX.\" Simple ask. It turned into a genuinely interesting engineering problem, and\n\nI ended up building a small open-source MCP server to solve it. Here's the\n\nchallenge, the solution, and the traps along the way.\n\nThe result is open source (MIT): ** loop-reader-mcp**.\n\nTwo walls, right at the start.\n\n**Wall 1: Microsoft Loop has no content API.** As of mid-2026, there's no\n\nGraph endpoint to read or write a Loop page's content — no \"get page,\" no\n\n\"list workspace.\" Loop is positioned against Notion and Confluence, but you\n\ncan't programmatically get your own content out in a structured way. The 2026\n\nLoop roadmap is about governance, not a content API.\n\n**Wall 2: where Loop actually stores things.** Loop workspace pages live in\n\n**SharePoint Embedded (SPE)** containers, and Loop components from Teams/Outlook\n\nlive as `.loop`\n\nfiles in OneDrive. There *is* a documented trick: Microsoft\n\nGraph can convert a `.loop`\n\nfile to HTML on the fly with\n\n`GET /drives/{id}/items/{id}/content?format=html`\n\n. So reading is possible\n\nthrough the file layer even without a Loop API.\n\nBut then the real problem showed up — the one worth writing about.\n\nSharePoint Embedded **does not accept delegated (per-user) tokens** for content\n\ndownloads. Only an **app-only** identity can fetch the bytes.\n\nIf you build the naive thing — a service that uses its app identity to read\n\nLoop — you've created a permission-flattening machine. The app can read\n\n*everything*, so anyone who can talk to your service can read any Loop page in\n\nthe tenant, regardless of what they personally have access to. That's a data\n\nleak with extra steps. Unacceptable.\n\nSo the question became: **how do you let a service read content with an app\nidentity, while guaranteeing each user only sees what they're allowed to see?**\n\nThe insight that cracked it: **Graph Search is security-trimmed for delegated\ncallers, even for SPE content.** Search respects the user's permissions; only\n\nSo I split the two operations across two identities:\n\n**Discovery runs as the user.** When the assistant searches Loop, the server\n\nexchanges the user's token via the OAuth **On-Behalf-Of (OBO)** flow and\n\ncalls Graph Search *as that user*. Microsoft trims the results to exactly\n\nwhat they can access. The server records the `(driveId, itemId)`\n\nof every\n\nhit in a short-lived, per-user cache.\n\n**Retrieval is gated by that discovery.** When the assistant asks to read a\n\npage, the server refuses unless that exact `(driveId, itemId)`\n\npair is in\n\n*this user's* cache — i.e. unless they personally just discovered it via\n\ntheir own trimmed search. Only then does it use the app identity to fetch\n\nand convert the bytes.\n\n``` php\nsearch  -- OBO (user identity) --> Graph Search  -> results trimmed by Microsoft\n                                                  -> (driveId,itemId) cached per user\nread    -- is this pair in the caller's cache? --> no  -> refuse (no Graph call)\n                                                   yes -> app identity -> ?format=html\n```\n\n**The authorization decision is Microsoft's, not mine.** A user can't discover\n\na page they can't access (search runs as them), and can't read a page they\n\ndidn't discover. The app identity is just a retrieval mechanism for bytes the\n\nuser already proved they can see. Permission flattening solved.\n\nI exposed this as a remote **Model Context Protocol** server with three\n\nread-only tools: `loop_search`\n\n, `loop_list_components`\n\n, and `loop_get_page`\n\n.\n\nRead-only isn't a policy toggle — the Graph client only permits `GET`\n\nand\n\n`POST /search/query`\n\n, so there's structurally no way to write. (Good, because\n\noverwriting a `.loop`\n\nfile with anything else corrupts it, and there's no\n\nsupported write API anyway.)\n\nFor auth, I initially tried to put a platform \"easy auth\" gateway in front of\n\nthe server. **Big mistake** — the gateway intercepted the OAuth handshake and\n\nthe MCP client could never discover where to log in. The fix was to make the\n\nserver its own OAuth resource server: it publishes the discovery documents\n\n(`/.well-known/oauth-protected-resource`\n\nand `/.well-known/oauth-authorization-server`\n\n)\n\npointing clients at Microsoft Entra, and it validates the incoming token itself\n\n(signature via Entra's JWKS, audience, issuer, expiry). No gateway, no\n\ninterception, and access is still fully gated because Entra only issues tokens\n\nto users assigned to the app.\n\n`(driveId, itemId)`\n\npairs, not just the item IDThe code is on GitHub under MIT: ** github.com/DenizV/loop-reader-mcp**. The README covers the\n\n`SECURITY.md`\n\ndocuments the model andIt's community code, not a certified product — review it and run a dependency\n\nscan before you point it at real data. But if you've been wanting to let an\n\nassistant *read* your Loop content without handing it the keys to the whole\n\ntenant, this pattern works.\n\n*If you build on it or find a sharper approach, I'd love to hear it.*", "url": "https://wpnews.pro/news/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking", "canonical_source": "https://dev.to/denizv/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking-permissions-3adc", "published_at": "2026-07-18 01:03:39+00:00", "updated_at": "2026-07-18 01:27:49.557850+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["Microsoft Loop", "SharePoint Embedded", "Microsoft Graph", "OneDrive", "loop-reader-mcp", "Model Context Protocol", "OAuth On-Behalf-Of"], "alternates": {"html": "https://wpnews.pro/news/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking", "markdown": "https://wpnews.pro/news/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking.md", "text": "https://wpnews.pro/news/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking.txt", "jsonld": "https://wpnews.pro/news/giving-an-ai-assistant-read-only-access-to-microsoft-loop-without-breaking.jsonld"}}