feat(microsoft-teams): new MCP — channel & chat messaging, meetings, triggers#444
Merged
viktormarinho merged 5 commits intoMay 22, 2026
Merged
Conversation
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>
…nostics + 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>
…ptions 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>
Contributor
There was a problem hiding this comment.
11 issues found across 27 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="microsoft-teams/server/lib/graph-client.ts">
<violation number="1" location="microsoft-teams/server/lib/graph-client.ts:71">
P1: graphFetch only handles HTTP 204 as an empty-body success, but Microsoft Graph action endpoints (e.g., `POST /me/events/{id}/cancel`, `POST /me/events/{id}/accept`, `POST /me/events/{id}/decline`) return `202 Accepted` with an empty body. Calling `response.json()` on an empty body throws `SyntaxError: Unexpected end of JSON input`, causing successful API calls to be reported as errors.</violation>
<violation number="2" location="microsoft-teams/server/lib/graph-client.ts:304">
P2: Graph $search expression does not escape double quotes or backslashes in user-provided query, which can produce malformed search syntax per Microsoft Graph documentation.</violation>
<violation number="3" location="microsoft-teams/server/lib/graph-client.ts:568">
P1: `listJoinedTeams` calls the wrong Graph API endpoint. It uses `/teams` but should use `/me/joinedTeams` to list teams the authenticated user is a member of, matching the function's intent and the `LIST_TEAMS` tool description. The `/teams` endpoint does not support listing all joined teams without a group ID.</violation>
</file>
<file name="microsoft-teams/server/main.ts">
<violation number="1" location="microsoft-teams/server/main.ts:30">
P2: OAuth tenant defaults to `common`, allowing unsupported personal Microsoft accounts</violation>
<violation number="2" location="microsoft-teams/server/main.ts:83">
P2: Authorization code exchange coerces missing refresh_token to empty string, creating an invalid persisted token that breaks future refresh attempts.</violation>
</file>
<file name="microsoft-teams/server/tools/meetings.ts">
<violation number="1" location="microsoft-teams/server/tools/meetings.ts:512">
P2: DECLINE_MEETING and TENTATIVELY_ACCEPT_MEETING duplicate nearly identical schema definitions and execute logic. Extract a shared helper/factory for the common respond-to-invitation pattern to avoid future divergence.</violation>
<violation number="2" location="microsoft-teams/server/tools/meetings.ts:558">
P1: Partial proposed_start/proposed_end inputs are silently ignored in decline and tentative-accept meeting responses. Both fields are optionally declared, but the code only includes `proposedNewTime` when BOTH are present. If only one is provided, it is silently dropped and `{ success: true }` is returned, causing the organizer to never receive the proposed time with no feedback to the caller. This same bug exists in both `createDeclineMeetingTool` and `createTentativelyAcceptMeetingTool`.</violation>
</file>
<file name="microsoft-teams/server/tools/subscriptions.ts">
<violation number="1" location="microsoft-teams/server/tools/subscriptions.ts:57">
P1: `clientState` webhook secret is generated with non-cryptographic `Math.random()`, weakening the only verification mechanism for unauthenticated Graph webhook notifications.</violation>
</file>
<file name="microsoft-teams/server/lib/errors.ts">
<violation number="1" location="microsoft-teams/server/lib/errors.ts:101">
P2: Non-Error thrown values are normalized with unguarded `String(err)`, which loses object detail (produces `[object Object]`) and can throw on hostile values. Project feedback requires `JSON.stringify` with fallback.</violation>
</file>
<file name="microsoft-teams/server/lib/logger.ts">
<violation number="1" location="microsoft-teams/server/lib/logger.ts:47">
P1: Unguarded `JSON.stringify` in `emit` can throw and mask original errors when `ctx` contains circular references or throwing `toJSON`.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
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>
Contributor
There was a problem hiding this comment.
2 issues found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="microsoft-teams/server/lib/graph-client.ts">
<violation number="1" location="microsoft-teams/server/lib/graph-client.ts:304">
P2: Graph $search expression does not escape double quotes or backslashes in user-provided query, which can produce malformed search syntax per Microsoft Graph documentation.</violation>
</file>
<file name="microsoft-teams/server/main.ts">
<violation number="1" location="microsoft-teams/server/main.ts:30">
P2: OAuth tenant defaults to `common`, allowing unsupported personal Microsoft accounts</violation>
</file>
<file name="microsoft-teams/server/lib/errors.ts">
<violation number="1" location="microsoft-teams/server/lib/errors.ts:101">
P2: Non-Error thrown values are normalized with unguarded `String(err)`, which loses object detail (produces `[object Object]`) and can throw on hostile values. Project feedback requires `JSON.stringify` with fallback.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Contributor
There was a problem hiding this comment.
1 issue found across 7 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
07ddc41 to
751f660
Compare
- 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>
751f660 to
14c0725
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New Microsoft Teams MCP integrating with the Microsoft Graph API using deco's native delegated OAuth (Authorization Code + PKCE — users connect via the "Connect to Microsoft" button).
Capabilities
teams.message.receivedvia Graph change notifications (subscribe/refresh/unsubscribe + event diagnostics)Hardening
Summary by cubic
Adds a new Microsoft Teams MCP for channel/chat messaging, meeting management, and a
teams.message.receivedtrigger. Runs on Cloudflare Workers with Workers KV for state and reliable trigger delivery, with security and correctness hardening.New Features
teams.message.receivedvia Graph change notifications with tools to subscribe/list/refresh/unsubscribe (auto-renew near expiry).@decocms/runtime(“Connect to Microsoft”). Requires a Microsoft 365 work/school account.Refactors
clientState.waitUntil()instead of fire-and-forget.trace_idand safe JSON; centralized error formatting with hints./me/joinedTeams; reactions send proper emoji; avoid persisting emptyrefresh_token.wrangler.toml(real KV namespace id), a deploy workflow, and updated scripts/types for Workers.Written for commit 14c0725. Summary will update on new commits. Review in cubic