Skip to main content
Glama
adkins-amdg

Microsoft Teams MCP Server

by adkins-amdg
README.md
# Microsoft Teams MCP Server

An MCP server that gives an AI agent access to **Microsoft Teams via Microsoft Graph**, using **delegated, device-code authentication** — the server acts *as you*, so it can only touch what your own Teams account can already see (least-privilege by default).

Built with the official MCP TypeScript SDK + MSAL. Runs locally over stdio.

## What it can do

| Scope | Tools |
| --- | --- |
| **Messages** | `teams_list_chats`, `teams_get_chat_messages`, `teams_send_chat_message`, `teams_list_channel_messages`, `teams_send_channel_message`, `teams_reply_channel_message` |
| **Teams & channels** | `teams_list_joined_teams`, `teams_list_channels`, `teams_list_team_members`, `teams_create_channel` |
| **Meetings & calendar** | `teams_create_meeting`, `teams_list_events`, `teams_get_event` |
| **Search & files** | `teams_search_messages`, `teams_list_channel_files` |
| **Identity** | `teams_whoami` |

Every read tool supports `response_format: "markdown" | "json"` and a `limit`. Write tools (`send_*`, `reply_*`, `create_*`) are annotated non-read-only so clients can gate them.

---

## 1. Entra ID setup (one time)

You need an **app registration** in Microsoft Entra ID (Azure AD). You don't need to be a global admin to *create* it, but some delegated scopes are admin-restricted (see below).

1. Go to **Entra ID → App registrations → New registration**.
   - Name: `Teams MCP (delegated)`
   - Supported account types: *Accounts in this organizational directory only* (single tenant) is fine.
   - Redirect URI: leave blank.
2. On the new app, open **Authentication** →
   - Under **Advanced settings**, set **Allow public client flows** = **Yes**. (Required for device-code.)
3. Open **API permissions → Add a permission → Microsoft Graph → Delegated permissions** and add:
   - `User.Read`
   - `Chat.ReadWrite`
   - `ChannelMessage.Send`
   - `ChannelMessage.Read.All` *(admin consent)*
   - `Team.ReadBasic.All`
   - `Channel.ReadBasic.All`
   - `Channel.Create`
   - `TeamMember.Read.All` *(admin consent)*
   - `Calendars.ReadWrite`
   - `OnlineMeetings.ReadWrite`
   - `Files.Read.All`
   - `Sites.Read.All`
   - `offline_access` (usually added automatically)
4. Click **Grant admin consent** for the tenant. (The *.All* read scopes won't work without it. If you're not an admin, send your Teams/M365 admin this list to consent.)
5. Copy the **Application (client) ID** and your **Directory (tenant) ID** from the app's Overview page.

> **Why delegated, not application permissions?** Application (app-only) permissions would grant the server org-wide read of *every* chat in the tenant — a large PHI/privacy blast radius and a harder security review. Delegated keeps the server scoped to your own access. If you later need unattended automation, add a client-credentials path then.

---

## 2. Install & build

```bash
cd teams-mcp-server
npm install
npm run build
```

## 3. Configure environment

```bash
cp .env.example .env
# edit .env: set TEAMS_MCP_CLIENT_ID and TEAMS_MCP_TENANT_ID
```

Or export the variables in your shell / MCP client config (below).

## 4. Sign in (one time)

```bash
export TEAMS_MCP_CLIENT_ID=<your-app-client-id>
export TEAMS_MCP_TENANT_ID=<your-tenant-id>     # or "organizations"
npm run login
```

You'll see a message like *"To sign in, use a web browser to open https://microsoft.com/devicelogin and enter the code XXXXXXXX."* Complete it in your browser. The token cache (including a refresh token) is saved to `~/.teams-mcp-token-cache.json`, so you won't need to do this again until the refresh token expires or is revoked.

## 5. Connect it to your MCP client

Point your MCP client at the built server over stdio. Example client config:

```json
{
  "mcpServers": {
    "teams": {
      "command": "node",
      "args": ["/absolute/path/to/teams-mcp-server/dist/index.js"],
      "env": {
        "TEAMS_MCP_CLIENT_ID": "<your-app-client-id>",
        "TEAMS_MCP_TENANT_ID": "<your-tenant-id>"
      }
    }
  }
}
```

If no cached account exists, the **first tool call** triggers an interactive device-code login printed to **stderr**. Running `npm run login` first avoids that.

---

## Testing

Use the MCP Inspector to exercise tools interactively:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

Start with `teams_whoami` to confirm auth, then `teams_list_joined_teams` → grab a `team_id` → `teams_list_channels`, etc.

## How auth works

- **MSAL `PublicClientApplication`** with the **device-code** flow.
- Tokens + refresh token cached to disk (`TEAMS_MCP_TOKEN_CACHE`, default `~/.teams-mcp-token-cache.json`, written `0600`).
- Each Graph call acquires a token **silently** from cache; only an expired/revoked refresh token forces a new device-code login.

## Notes & limits

- **Admin-restricted scopes:** `ChannelMessage.Read.All` and `TeamMember.Read.All` require tenant admin consent. Without it those specific tools return a clear `403` with guidance.
- **Search** uses the Microsoft Search API (`/search/query`, `chatMessage` entity) — relevance-ranked, not a literal grep.
- **Meetings** are created as calendar events with `isOnlineMeeting: true` (Teams provider), which puts them on your calendar, invites attendees, and returns a join link.
- **Channel files** resolve the channel's SharePoint `filesFolder`, then list its children.
- Responses are truncated at 25,000 characters with guidance to narrow the query.
- HIPAA note: this server can read message content that may contain PHI. It runs locally and holds no data beyond the on-disk token cache, but treat the host machine accordingly.

## Project layout

```
teams-mcp-server/
├── package.json
├── tsconfig.json
├── .env.example
├── README.md
└── src/
    ├── index.ts          # entry point + `login` command
    ├── constants.ts      # Graph base URL, scopes, limits
    ├── auth.ts           # MSAL device-code + disk token cache
    ├── graph.ts          # Graph request/pagination/error/format helpers
    ├── schemas.ts        # shared Zod fragments
    └── tools/
        ├── identity.ts   # teams_whoami
        ├── teams.ts      # teams & channels
        ├── messages.ts   # chat + channel messaging
        ├── meetings.ts   # meetings & calendar
        └── search.ts     # search + channel files
```

TDQS

A4.2/5.0

Scored across 16 tools

Disambiguation5/5

Every tool serves a distinct purpose: creating channels, meetings, sending/receiving messages in channels and chats, listing resources, etc. No two tools overlap in functionality, so an agent can easily select the correct one.

Naming Consistency5/5

All tools follow the exact pattern `teams_verb_noun` in snake_case. Verbs are uniformly imperative (create, get, list, reply, search, send, whoami), making the set predictable and easy to navigate.

Tool Count5/5

With 16 tools, the server covers the core Teams operations (team/channel management, messaging, meetings, events, files) without unnecessary bloat. The number is well-proportioned to the domain.

Completeness4/5

The surface enables most everyday workflows: reading and writing messages in channels and chats, listing teams/channels/members/events/files, creating channels and meetings, and searching. Minor gaps like updating or deleting messages or channels are absent, but the core is solid.

Maintenance

ActivityInactive
ResponsivenessNo issues