Skip to content

Commit b68101b

Browse files
feat(microsoft-teams): new MCP — channel & chat messaging, meetings, triggers (#444)
* feat(microsoft-teams): new MCP integrating with Microsoft Teams Adds a Microsoft Teams MCP that lets deco agents interact with Teams via the Microsoft Graph API. Authentication uses the deco runtime's native OAuth integration (Authorization Code flow with delegated permissions), so users sign in via the "Connect to Microsoft" button in deco Studio. Tools: - Channel: TEAMS_LIST_TEAMS, TEAMS_LIST_CHANNELS, TEAMS_SEND_MESSAGE (with optional subject), TEAMS_REPLY_TO_MESSAGE. - Chat (1-on-1 and group): TEAMS_LIST_CHATS (enriches oneOnOne chats with the other member's name), TEAMS_GET_CHAT_MEMBERS, TEAMS_LIST_CHAT_MESSAGES, TEAMS_SEND_CHAT_MESSAGE, TEAMS_FIND_USER, TEAMS_CREATE_PRIVATE_CHAT, TEAMS_CREATE_GROUP_CHAT. Trigger: - teams.message.received fired when a new message lands in a subscribed channel via Microsoft Graph change notifications. - Webhook routes at /teams/notifications/:connectionId handle the Graph validation handshake and notification dispatch, with auto-renewal of subscriptions near expiry. Infra: - KV store (file-backed JSON) for trigger storage and webhook-time credential lookup. - Standalone test scripts in scripts/ for CLI testing. Adds 'microsoft-teams' to the root workspaces list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(microsoft-teams): chat tools, channel reads, subscriptions, diagnostics + hardening Expands the Microsoft Teams MCP and cleans up the codebase. New capabilities: - Channel reads: LIST_CHANNEL_MESSAGES (optional include_replies via $expand), LIST_MESSAGE_REPLIES; message mutations EDIT_CHANNEL_MESSAGE and REACT_TO_CHANNEL_MESSAGE. - Private chat: LIST_CHATS (resolves the other member's name for 1-on-1), GET_CHAT_MEMBERS, SEND_CHAT_MESSAGE (with quote-reply via messageReference), LIST_CHAT_MESSAGES, FIND_USER, CREATE_PRIVATE_CHAT, CREATE_GROUP_CHAT, EDIT_CHAT_MESSAGE, REACT_TO_CHAT_MESSAGE. - Webhook subscription lifecycle (agent-driven): SUBSCRIBE_TO_CHANNEL, LIST_SUBSCRIPTIONS, REFRESH_SUBSCRIPTIONS, UNSUBSCRIBE_FROM_CHANNEL. - Diagnostics: GET_RECENT_EVENTS, CLEAR_RECENT_EVENTS for inspecting the trigger pipeline end-to-end. Hardening: - Structured logger with per-request trace_id and measure() timings. - Event deduplication for redelivered Graph notifications. - Centralized error formatting (errors.ts) with machine codes and actionable hints surfaced on every tool response. - Reactions send the Unicode emoji Graph expects; chat web_url is built client-side when Graph omits it. Cleanup: - Switch to the deco runtime's native OAuth (PKCE) — removed the custom /auth routes, config-cache, and standalone debug scripts. - Bump @decocms/runtime to ^1.6.0 to match the OAuth config contract. - Remove dead helpers (buildAuthorizeUrl, getUserProfile, delete-message tools) and run oxfmt across the package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(microsoft-teams): meeting management, user search, richer descriptions Adds calendar/meeting tools and improves user lookup. Meetings (Graph Calendar API, requires Calendars.ReadWrite delegated scope): - CREATE_MEETING (with Teams join link), LIST_MEETINGS, GET_MEETING, UPDATE_MEETING, RESCHEDULE_MEETING, DELETE_MEETING, CANCEL_MEETING, and invitation responses ACCEPT_MEETING, DECLINE_MEETING, TENTATIVELY_ACCEPT_MEETING. - Meeting output now includes the full `description` (HTML body stripped to plain text) alongside Graph's truncated `preview`. Users: - New SEARCH_USERS_BY_NAME (Graph $search) to resolve a person by name when the email is unknown. - Renamed FIND_USER → GET_USER_BY_EMAIL for clarity (exact-email lookup vs name search). Docs: - Update app.json to reflect delegated OAuth, the current toolset, and the Microsoft 365 work/school account requirement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(microsoft-teams): make MCP Cloudflare Workers-ready Migrates the Teams MCP to run on Cloudflare Workers, using the github and google-gmail MCPs as the reference pattern. Runtime / entry: - Replace the Bun serve() server with an `export default { fetch }` Worker entrypoint; the runtime is built lazily as a singleton. - Webhook routes are handled in the fetch handler before falling through to runtime.fetch(), with env.TEAMS_KV threaded into the KV store per request. Storage (Workers isolates are ephemeral): - Rewrite lib/kv.ts as a Cloudflare KV adapter (same get/set/delete/keys interface, TTL via expirationTtl); drop the Bun.file disk store. - Move dedup to KV (async) instead of an in-memory Map. Trigger delivery correctness on Workers: - Stop using the runtime's fire-and-forget triggers.notify() in the webhook path — it delivers via a floating promise the isolate cancels after the response. Add an awaitable deliverToMesh() (reads trigger credentials from KV and POSTs to the callback) and register the background work with ctx.waitUntil() so it completes. Mirrors the github webhook pattern. Config / tooling: - Add wrangler.toml (nodejs_compat, TEAMS_KV namespace, custom domain). - Switch package.json scripts to wrangler dev/deploy; add wrangler and @cloudflare/workers-types; tsconfig picks up workers-types. - Add a dedicated deploy-microsoft-teams.yml workflow (same shape as deploy-github.yml) — wrangler deploy on push to microsoft-teams/**. - env.ts gains the TEAMS_KV binding and MICROSOFT_* secret types. - Point app.json connection URL and webhook defaults at the microsoft-teams-mcp.decocms.com Worker domain; fix the broken icon URL. - Add README.md; ignore .wrangler/ and .dev.vars. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(microsoft-teams): address review findings - security: generate webhook clientState with crypto.randomUUID() instead of Math.random() (it is the only verification for unauthenticated Graph notifications). - graph-client: handle empty-body success responses (202/204) — action endpoints like cancel/accept/decline/setReaction return 202 with no body, which previously threw on response.json() and reported false failures. - graph-client: listJoinedTeams now calls /me/joinedTeams (the user's teams) instead of /teams (org-wide, needs admin/app permissions). - meetings: reject partial proposed_start/proposed_end in decline and tentative responses with a clear validation error instead of silently dropping the proposed time; extract a shared createRespondTool factory for accept/decline/tentative to remove duplication. - main: do not coerce a missing refresh_token to "" on code exchange — leave it undefined so an invalid empty token isn't persisted and future refreshes aren't broken. - logger: serialize log lines defensively (safeStringify) so circular refs, throwing toJSON, or bigint never crash a handler or mask the original error. - chore: drop the orphaned `publish` script (deco-cli was removed in the Workers migration; registry publish is handled by CI). - wrangler.toml: set the real TEAMS_KV namespace id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 616c2cf commit b68101b

30 files changed

Lines changed: 4984 additions & 0 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy Microsoft Teams MCP
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- "microsoft-teams/**"
10+
- ".github/workflows/deploy-microsoft-teams.yml"
11+
12+
jobs:
13+
deploy:
14+
runs-on: ubuntu-latest
15+
defaults:
16+
run:
17+
working-directory: microsoft-teams
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: oven-sh/setup-bun@v2
23+
with:
24+
bun-version: latest
25+
26+
- name: Cache Bun dependencies
27+
uses: actions/cache@v4
28+
with:
29+
path: ~/.bun/install/cache
30+
key: ${{ runner.os }}-bun-microsoft-teams-${{ hashFiles('microsoft-teams/bun.lock', 'microsoft-teams/package.json') }}
31+
restore-keys: ${{ runner.os }}-bun-microsoft-teams-
32+
33+
- name: Install dependencies
34+
run: bun install --frozen-lockfile
35+
36+
- name: Type check
37+
run: bun run check
38+
continue-on-error: true
39+
40+
- name: Deploy to Cloudflare Workers
41+
run: bunx wrangler deploy
42+
env:
43+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
44+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

bun.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

microsoft-teams/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules
2+
dist
3+
.env
4+
.env.*
5+
!.env.example
6+
.dev.vars
7+
.dev.vars.*
8+
!.dev.vars.example
9+
.secrets.json
10+
.wrangler/
11+
*.log

microsoft-teams/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Microsoft Teams MCP
2+
3+
Microsoft Teams integration via the Microsoft Graph API — channel and private
4+
chat messaging, meeting management, and a message-received trigger. Authenticates
5+
with Azure AD delegated OAuth (Authorization Code + PKCE); all actions run on
6+
behalf of the connected Microsoft 365 user.
7+
8+
## Features
9+
10+
- **Channel messaging** — list teams/channels, send & reply, read history &
11+
threads, edit, react
12+
- **Private chats** — find/search users (by email or name), open 1-on-1 & group
13+
chats, send & quote-reply, read history
14+
- **Meetings** — create Teams meetings with join links, list, get, update,
15+
reschedule, cancel, and respond (accept / decline / tentative)
16+
- **OAuth Authentication** — connect your Microsoft 365 account with one click
17+
- **Trigger System**`teams.message.received` fires when a new message is
18+
posted in a subscribed channel (Graph change notifications)
19+
20+
## Trigger Events
21+
22+
- `teams.message.received` — a new message was posted in a subscribed channel.
23+
Manage subscriptions with `SUBSCRIBE_TO_CHANNEL`, `REFRESH_SUBSCRIPTIONS`,
24+
`LIST_SUBSCRIPTIONS`, `UNSUBSCRIBE_FROM_CHANNEL`. Inspect delivered events
25+
with `GET_RECENT_EVENTS`.
26+
27+
## Architecture
28+
29+
```
30+
Client → MCP runtime (this Worker) → graph.microsoft.com
31+
Graph change notifications → /teams/notifications/:connectionId → trigger matching
32+
```
33+
34+
State (trigger subscriptions, cached tokens, dedup, event log) is persisted in
35+
a Cloudflare Workers KV namespace because Workers isolates are ephemeral.
36+
37+
> Note: Microsoft Teams APIs require a Microsoft 365 work or school account.
38+
> Personal Microsoft accounts (Outlook/Hotmail) are not supported.
39+
40+
---
41+
42+
## Development
43+
44+
### Prerequisites
45+
46+
An **Azure AD app registration** (multi-tenant) with delegated Microsoft Graph
47+
permissions and admin consent:
48+
49+
- `User.Read`, `User.ReadBasic.All`
50+
- `ChannelMessage.Send`, `ChannelMessage.Read.All`, `ChannelMessage.ReadWrite`
51+
- `Channel.ReadBasic.All`, `Team.ReadBasic.All`
52+
- `Chat.ReadWrite`, `ChatMessage.Send`
53+
- `Calendars.ReadWrite`
54+
- `offline_access`
55+
56+
Register the OAuth redirect URI (`/oauth/callback`) under
57+
Authentication → Web in the app registration.
58+
59+
### Environment Variables
60+
61+
Set as Worker secrets (`wrangler secret put`) or, for local dev, in `.dev.vars`:
62+
63+
```bash
64+
MICROSOFT_TENANT_ID=organizations # or your specific tenant id
65+
MICROSOFT_CLIENT_ID=<azure-app-client-id>
66+
MICROSOFT_CLIENT_SECRET=<azure-app-client-secret>
67+
```
68+
69+
### Running locally
70+
71+
```bash
72+
bun install
73+
bunx wrangler dev # local Worker at http://localhost:8787 (KV simulated)
74+
bun run check # type check
75+
bun run build # production bundle (wrangler dry-run)
76+
```
77+
78+
Expose the local Worker with a tunnel (e.g. `ngrok http 8787`) and register the
79+
public `/mcp` URL as a custom connection in deco Studio.
80+
81+
### Deploy (Cloudflare Workers)
82+
83+
```bash
84+
bunx wrangler kv namespace create TEAMS_KV # paste the id into wrangler.toml
85+
bunx wrangler secret put MICROSOFT_TENANT_ID
86+
bunx wrangler secret put MICROSOFT_CLIENT_ID
87+
bunx wrangler secret put MICROSOFT_CLIENT_SECRET
88+
bun run deploy
89+
```

microsoft-teams/app.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"scopeName": "deco",
3+
"name": "microsoft-teams",
4+
"friendlyName": "Microsoft Teams",
5+
"connection": {
6+
"type": "HTTP",
7+
"url": "https://microsoft-teams-mcp.decocms.com/mcp"
8+
},
9+
"description": "Microsoft Teams integration — send channel and private chat messages, manage meetings, and trigger agents on new Teams messages via the Microsoft Graph API.",
10+
"icon": "https://upload.wikimedia.org/wikipedia/commons/9/94/Microsoft_Office_Teams_%282019%E2%80%932025%29.svg",
11+
"unlisted": false,
12+
"metadata": {
13+
"categories": ["Communication", "Automation"],
14+
"official": false,
15+
"tags": ["teams", "microsoft", "bot", "messaging", "ai-agent", "webhooks", "automation", "channels", "chats", "meetings", "calendar", "azure"],
16+
"short_description": "Microsoft Teams bot — channel & private messages, meetings, and message-received triggers via Microsoft Graph.",
17+
"mesh_description": "The Microsoft Teams MCP integrates with Microsoft Teams through the Microsoft Graph API. Authentication uses Azure AD delegated OAuth (Authorization Code flow with PKCE) — users sign in with the 'Connect to Microsoft' button in deco Studio, and all actions run on behalf of that work or school account (Microsoft 365). Capabilities include: channel messaging (list teams and channels, send and reply to messages, read channel history and threads, edit and react to messages); private chats (find or search users by email or name, open 1-on-1 and group chats, send and quote-reply to chat messages, read chat history); and meeting management (create Teams meetings with join links, list, get, update, reschedule, cancel, and respond to invitations with accept/decline/tentative). A trigger system delivers the 'teams.message.received' event whenever a new message is posted in a subscribed channel via Graph change notifications, giving the agent full context (sender, text, thread, web link) so it can respond. Note: Teams APIs require a Microsoft 365 work or school account; personal Microsoft accounts are not supported."
18+
}
19+
}

microsoft-teams/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "microsoft-teams-mcp",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"private": true,
6+
"description": "Microsoft Teams MCP — send messages, manage meetings and react to Teams events via Microsoft Graph API",
7+
"scripts": {
8+
"dev": "bunx wrangler dev",
9+
"check": "tsc --noEmit",
10+
"build": "bunx wrangler deploy --dry-run --outdir=dist",
11+
"deploy": "bunx wrangler deploy"
12+
},
13+
"dependencies": {
14+
"@decocms/bindings": "^1.4.0",
15+
"@decocms/runtime": "^1.6.0",
16+
"hono": "^4.7.4",
17+
"zod": "^4.0.0"
18+
},
19+
"devDependencies": {
20+
"@cloudflare/workers-types": "^4.20251014.0",
21+
"@decocms/mcps-shared": "1.0.0",
22+
"@types/node": "^22.0.0",
23+
"typescript": "^5.7.2",
24+
"wrangler": "^4.28.0"
25+
},
26+
"engines": {
27+
"node": ">=22.0.0"
28+
},
29+
"overrides": {
30+
"zod": "^4.0.0"
31+
}
32+
}

microsoft-teams/server/lib/auth.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Token accessor for the Microsoft Teams MCP.
3+
*
4+
* The deco runtime handles the entire OAuth flow (authorize, exchange,
5+
* refresh) — the per-request bearer token arrives in
6+
* `env.MESH_REQUEST_CONTEXT.authorization`. We just pull it out here.
7+
*
8+
* For webhook handlers that run *outside* a tool request (no MESH context),
9+
* use getDelegatedTokenForConnection() with a stored refresh token.
10+
*/
11+
12+
import type { Env } from "../types/env.ts";
13+
import { getKvStore } from "./kv.ts";
14+
15+
/** Pull the user's bearer token off the per-request mesh context. */
16+
export function getAccessToken(env: Env): string {
17+
const auth = env.MESH_REQUEST_CONTEXT?.authorization;
18+
if (!auth) {
19+
throw new Error(
20+
"Missing authorization. Click 'Connect to Microsoft' in deco Studio first.",
21+
);
22+
}
23+
// Authorization header may come as "Bearer <token>" or just "<token>"
24+
return auth.startsWith("Bearer ") ? auth.slice(7) : auth;
25+
}
26+
27+
/**
28+
* For webhook handlers (no MESH request context, so we can't read
29+
* MESH_REQUEST_CONTEXT.authorization). Reads an access token cached by
30+
* SUBSCRIBE_TO_CHANNEL / REFRESH_SUBSCRIPTIONS.
31+
*
32+
* Throws if the cache is missing or expired — the agent must call
33+
* REFRESH_SUBSCRIPTIONS to reseed the cache (and renew the Graph
34+
* subscription itself, which expires every ~60 min anyway).
35+
*/
36+
export async function getDelegatedTokenForConnection(
37+
connectionId: string,
38+
): Promise<string> {
39+
const kv = getKvStore();
40+
const cached = await kv.get<{ accessToken: string; expiresAt: number }>(
41+
`webhook-token:${connectionId}`,
42+
);
43+
44+
if (!cached) {
45+
throw new Error(
46+
`No cached webhook token for connection ${connectionId}. ` +
47+
`Call SUBSCRIBE_TO_CHANNEL or REFRESH_SUBSCRIPTIONS to seed it.`,
48+
);
49+
}
50+
51+
if (Date.now() > cached.expiresAt - 60_000) {
52+
throw new Error(
53+
`Webhook token for connection ${connectionId} is expired. ` +
54+
`Call REFRESH_SUBSCRIPTIONS to renew.`,
55+
);
56+
}
57+
58+
return cached.accessToken;
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Event deduplication for Microsoft Graph notifications, backed by KV.
3+
*
4+
* Graph occasionally redelivers the same change notification (network blips,
5+
* subscription renewals, isolate restarts). Without dedup the agent would
6+
* receive the trigger twice and reply twice. We fingerprint each notification
7+
* and skip duplicates seen within a TTL window.
8+
*
9+
* Backed by Workers KV (not an in-memory Map) so dedup survives across the
10+
* ephemeral isolates that handle webhook deliveries.
11+
*/
12+
13+
import { getKvStore } from "./kv.ts";
14+
15+
const PREFIX = "dedup:";
16+
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1h — Graph retries within minutes
17+
18+
/** Build a fingerprint for a single Graph notification. */
19+
export function fingerprintNotification(notification: {
20+
subscriptionId: string;
21+
changeType: string;
22+
resourceData?: { id?: string };
23+
resource?: string;
24+
}): string {
25+
const resourceId =
26+
notification.resourceData?.id ?? notification.resource ?? "";
27+
return `${notification.subscriptionId}|${notification.changeType}|${resourceId}`;
28+
}
29+
30+
/**
31+
* Returns true if this fingerprint was already seen within the TTL window.
32+
* If new, records it (with TTL) and returns false so the caller processes it.
33+
*/
34+
export async function isDuplicateNotification(
35+
fingerprint: string,
36+
ttlMs: number = DEFAULT_TTL_MS,
37+
): Promise<boolean> {
38+
const kv = getKvStore();
39+
const key = `${PREFIX}${fingerprint}`;
40+
const seen = await kv.get<number>(key);
41+
if (seen) return true;
42+
await kv.set(key, Date.now(), ttlMs);
43+
return false;
44+
}

0 commit comments

Comments
 (0)