msloop-mcp
OfficialREADME.md
# msloop-mcp
MCP server for **Microsoft Loop**: enumerate, read, and edit your Loop workspaces and pages from any MCP client (Claude Desktop, Cursor, or anything else that speaks stdio).
No app registration required. Two auth modes: **browser-session** (reuses your signed-in Loop web app session — works even on tenants that block user consent) and **device-code** (Microsoft's first-party Graph PowerShell app). Tokens are cached locally and refresh automatically. See [Authentication](#authentication).
```
┌────────────┐ stdio ┌──────────────────────────────────────────────────┐
│ MCP client │ ◄──────► │ msloop-mcp │
└────────────┘ │ ├─ Graph search .loop files, HTML render │
│ ├─ Substrate workspaces + page discovery │
│ └─ LWS page read/create/edit (markdown) │
└──────────────────────────────────────────────────┘
```
## Setup
```bash
bun install && bun run build # produces the standalone binary dist/msloop-mcp
```
Install it on your PATH (no npm/node_modules needed at runtime — it's a self-contained executable):
```bash
ln -sf "$PWD/dist/msloop-mcp" ~/.local/bin/msloop-mcp
```
Then register it with your MCP client as a stdio server. Most clients take the same JSON shape — for Claude Desktop it goes in `claude_desktop_config.json`, for Cursor in `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"msloop": {
"type": "stdio",
"command": "msloop-mcp"
}
}
}
```
If the binary isn't on your PATH, use the full path in `command` instead.
**First run:** in browser-session mode there is no in-server login — capture the session once via `scripts/save-session.mjs` (see [Authentication](#authentication)). In device-code mode, call the `loop_login` tool: it returns a URL (`https://login.microsoft.com/device`) and a code — open the URL, sign in with your work/school Microsoft account, enter the code, approve. One approval covers all API planes; everything else is silent afterwards. Cache lives in `~/.msloop-mcp/` (mode 0600); `loop_logout` wipes it.
Requires a work/school account (Loop needs M365). [Bun](https://bun.sh) >= 1.1 to build and run tests.
## Authentication
Two modes, selected by `MSLOOP_AUTH_MODE` (`browser` | `device`). Auto-detected by default: **browser** when `~/.msloop-mcp/session-tokens.json` exists, **device** otherwise.
### Browser-session mode (default once a session is captured)
Reuses the signed-in Loop web app session — **no app registration, no admin consent**, so it works on tenants that block user consent (e.g. university tenants).
1. Open <https://loop.cloud.microsoft> in a dedicated browser profile and sign in with your work/school account, e.g.:
```bash
chromium --user-data-dir=~/.msloop-mcp/chromium-profile https://loop.cloud.microsoft
```
2. Dump the localStorage + sessionStorage entries whose keys match `/token/i` from the loop.cloud.microsoft tab into a JSON file — either an array of `{name, value}` or `{localStorage: {…}, sessionStorage: {…}}`.
3. Import it:
```bash
bun scripts/save-session.mjs entries.json
```
Tokens auto-refresh via the Loop SPA refresh token (client `a187e399-0c36-4b98-8f04-1edc167a0996`; the refresh grant sends `Origin: https://loop.cloud.microsoft`, which Entra requires for SPA clients), so **reads keep working indefinitely** after the one-time capture. If the session fully expires, redo the capture — `loop_auth_status` tells you when.
### Device-code mode (default otherwise, or `MSLOOP_AUTH_MODE=device`)
Signs in through Microsoft's first-party Graph PowerShell app, so no app registration is needed — but it **requires user consent for the Graph scopes**. Tenants with consent disabled (AADSTS65005-type admin-approval walls) cannot use this mode; use browser-session mode there.
Call `loop_login`, open the returned URL, enter the code, approve (details under [Setup](#setup)).
## Tools
| Tool | What it does |
|---|---|
| `loop_login` | Device-code sign-in (returns URL + code); in browser mode, returns capture instructions |
| `loop_auth_status` | Account + per-plane token state |
| `loop_logout` | Clear cached tokens |
| `loop_list_workspaces` | All workspaces (personal "My workspace" + shared), with ids |
| `loop_search` | Keyword search over every accessible Loop page/component; returns `pageId`s |
| `loop_list_pages` | Pages inside one workspace (by id or title substring) |
| `loop_read_page` | Page content as markdown (pageId, Loop URL, or title substring) |
| `loop_create_page` | New page with markdown content in a workspace |
| `loop_edit_page` | `append` (default, safest) · `replace_section` (one heading) · `replace_all` (requires `confirm: true`) |
| `loop_delete_page` | Move page to recycle bin (requires `confirm: true`) |
Page references accepted everywhere: `pageId` (base64 `host,driveId,itemId`), a Loop share URL (`https://…/:fl:/…`), or a title substring (unambiguous matches auto-resolve; ambiguous ones error with the candidates).
## How editing works
Loop has no public content API, so this server uses the same internal planes the Loop web app uses:
- **Read**: Loop Web Service markdown render, with fallback to Graph's `?format=html` render converted to markdown.
- **Write** (`create`/`edit`): LWS `PATCH /pages/{id}` with `location` semantics — `after` (append), `REPLACE_ALL`, or `TargetLabel` (section replace). Writes take markdown natively; `append` never touches existing content.
Editing a page while it is open in someone's browser is safe (Fluid merges text), but treat `replace_all` as destructive: embedded images/components in the replaced range are dropped.
## Configuration
| Env var | Default | Purpose |
|---|---|---|
| `MSLOOP_AUTH_MODE` | auto: `browser` if `~/.msloop-mcp/session-tokens.json` exists, else `device` | Force `browser` or `device` authentication |
| `MSLOOP_CLIENT_ID` | `14d82eec-…` (Graph PowerShell) | Use your own app registration |
| `MSLOOP_TENANT_ID` | `common` | Pin a tenant (recommended for own registrations) |
| `MSLOOP_CACHE_DIR` | `~/.msloop-mcp` | Token cache location |
| `MSLOOP_SEARCH_REGION` | unset | Graph Search `region` override (try `NAM`/`EUR`/`APC` if search errors mention a missing region) |
| `MSLOOP_GRAPH_BASE` / `MSLOOP_SUBSTRATE_BASE` / `MSLOOP_LWS_BASE` | production | API endpoints (overridable for testing) |
If your tenant blocks the shared first-party app: browser-session mode usually avoids the problem entirely (it uses Loop's own SPA client and needs no consent). If you must stay on device mode, register an app in Entra ID (public client, "Allow public client flows: Yes"), add delegated `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `User.Read`, then set `MSLOOP_CLIENT_ID`/`MSLOOP_TENANT_ID`.
## Known limits
- LWS pages/lists are a moving internal API; unexpected responses surface as descriptive errors, and the Graph fallback path keeps reads working regardless.
- `loop_create_page`/`loop_edit_page` use the Loop internal write API (LWS), which some tenants reject (EDU tenants are a common case) — the error says so explicitly. On those tenants pages can only be written from the Loop app; reads are unaffected.
- Advanced components (code canvases, Copilot notes) read as best-effort text.
## Tests
```bash
bun run typecheck && bun test/integration.mjs # stubbed-API end-to-end over InMemoryTransport
bun test/smoke.mjs # real compiled binary over stdio
bun test/e2e-live.mjs # live reads against your tenant (requires a captured session)
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues