DEV Community

Local-MCP
Local-MCP

Posted on AI-assisted

How we read Microsoft Teams messages without touching the Graph API

Quick context if you haven't played with MCP yet: MCP (Model Context Protocol) is the standard that lets AI assistants like Claude use tools, things like "search my email" or "read this file". An MCP server is the thing that exposes those tools. I work on LMCP, a free MCP server for Mac that connects Claude, Cursor, Codex and friends to the apps you already use.

Teams was the integration that gave us the most headaches, and the way we ended up solving it is kind of fun, so here it goes.

The annoying part about Graph

If you want Teams data in an AI assistant, the "official" way is Microsoft Graph. Which means: register an app in Entra ID, get admin consent, deal with OAuth tokens, respect rate limits.

That's fine if you run the tenant. But if you're just a regular person at a company where IT owns everything? You're stuck at "please ask your administrator" and that's the end of it.

The trick: Teams is secretly a website

New Teams isn't really a native app. It's a web app running inside WebView2, which is basically Microsoft's embedded Edge. And like any website running in a Chromium browser, it saves its data in IndexedDB. Chromium stores IndexedDB on disk as LevelDB.

So all your synced chats are already sitting on your Mac, here:

~/Library/Containers/com.microsoft.teams2/Data/Library/Application Support/Microsoft/MSTeams/EBWebView/WV2Profile_tfw*/IndexedDB/https_teams.microsoft.com_0.indexeddb.leveldb

Heads up if you go digging: lots of older posts point to EBWebView/Default/. That's outdated. Current builds create one folder per profile. The tfw ones are work/school accounts, and there's a tfl one for personal Teams that we just skip.

Actually reading the thing

LevelDB full of V8-serialized JavaScript objects is not something you want to parse by hand. Luckily there's ccl_chromium_reader, a Python library that comes from the digital forensics world (yes, the people who pull evidence out of browsers). It walks the LevelDB files, decodes the V8 objects and rebuilds the IndexedDB records. From there we grab chats, channels, messages and people.

Some stuff we learned the hard way:

  • Teams locks the database while it's running. We read the table files directly without grabbing the lock, so you don't have to close Teams.
  • Some records just refuse to decode. We skip them and keep a count, instead of blowing up the whole read.
  • Big accounts, big databases. The first read can take a bit.
  • Multiple work profiles? We read all of them and merge.
  • The schema moves around between Teams versions. Honestly this is the real pain. Every update might shuffle something and we have to go chase it.

What it looks like in practice

You ask Claude "what happened in #engineering today?" or "find the message where someone shared the deploy checklist", and it answers from what's already on your machine. No Graph permissions, no Microsoft login dance.

One thing people always ask: sending messages is a different story. That part does go through Graph, so you need to connect your Microsoft 365 account for it.

The honest limitations

  • It only sees what your Teams client has already synced. If Teams didn't pull a chat down, we can't see it either.
  • It depends on an undocumented format, so a Teams update can break it until we catch up.
  • You need the Teams desktop app installed and signed in.

Wanna try it?

It's free:

curl -fsSL 'https://local-mcp.com/install?ref=devto' | bash
Enter fullscreen mode Exit fullscreen mode

Or npx -y local-mcp@latest setup if you prefer npm. There's a longer walkthrough here.

If you've tried reading other Electron or WebView apps this way, I'd love to hear how it went. Happy to answer questions in the comments.

Top comments (0)