Skip to content

feat(microsoft-teams): new MCP — channel & chat messaging, meetings, triggers#444

Merged
viktormarinho merged 5 commits into
decocms:mainfrom
IgorFigueiredo28:igorfigueiredo/teams-mcp
May 22, 2026
Merged

feat(microsoft-teams): new MCP — channel & chat messaging, meetings, triggers#444
viktormarinho merged 5 commits into
decocms:mainfrom
IgorFigueiredo28:igorfigueiredo/teams-mcp

Conversation

@IgorFigueiredo28
Copy link
Copy Markdown
Contributor

@IgorFigueiredo28 IgorFigueiredo28 commented May 20, 2026

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

  • Channel messaging: list teams/channels, send & reply, read history & threads, edit, react
  • Private chats: find/search users (by email or name), open 1-on-1 & group chats, send & quote-reply, read history
  • Meetings: create (with Teams join link), list, get, update, reschedule, cancel, and respond (accept/decline/tentative)
  • Trigger: teams.message.received via Graph change notifications (subscribe/refresh/unsubscribe + event diagnostics)

Hardening

  • Structured logger with per-request trace_id + timings
  • Event deduplication for redelivered Graph notifications
  • Centralized error formatting with machine codes + actionable hints

Note: Microsoft Teams APIs require a Microsoft 365 work/school account; personal accounts are not supported (Graph limitation).


Summary by cubic

Adds a new Microsoft Teams MCP for channel/chat messaging, meeting management, and a teams.message.received trigger. Runs on Cloudflare Workers with Workers KV for state and reliable trigger delivery, with security and correctness hardening.

  • New Features

    • Channel messaging: list teams/channels; send/reply/edit/react; read history and threads.
    • Private chats: find users (email/name); create 1:1 or group chats; send and quote-reply; edit/react; read history.
    • Meetings: create (with Teams link); list/get; update/reschedule; cancel; respond (accept/decline/tentative).
    • Trigger: teams.message.received via Graph change notifications with tools to subscribe/list/refresh/unsubscribe (auto-renew near expiry).
    • Diagnostics: tools to list and clear recent webhook events (24h retention).
    • Auth: delegated OAuth via @decocms/runtime (“Connect to Microsoft”). Requires a Microsoft 365 work/school account.
  • Refactors

    • Migrated to Cloudflare Workers; webhook routes handled in the Worker fetch path with crypto UUID clientState.
    • State moved to Workers KV (triggers, subscriptions, webhook token cache, dedup, recent events).
    • Trigger delivery made reliable by awaiting callback work with waitUntil() instead of fire-and-forget.
    • KV-based dedup for Graph retries; structured JSON logger with per-request trace_id and safe JSON; centralized error formatting with hints.
    • Graph client hardening: handle 202/204 empty responses; use /me/joinedTeams; reactions send proper emoji; avoid persisting empty refresh_token.
    • Added 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

IgorFigueiredo28 and others added 3 commits May 18, 2026 19:22
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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread microsoft-teams/server/tools/subscriptions.ts Outdated
Comment thread microsoft-teams/server/lib/graph-client.ts Outdated
Comment thread microsoft-teams/server/lib/graph-client.ts Outdated
Comment thread microsoft-teams/server/tools/meetings.ts Outdated
Comment thread microsoft-teams/server/lib/logger.ts Outdated
Comment thread microsoft-teams/server/lib/errors.ts Outdated
Comment thread microsoft-teams/server/main.ts Outdated
Comment thread microsoft-teams/server/main.ts Outdated
Comment thread microsoft-teams/server/main.ts Outdated
Comment thread microsoft-teams/server/tools/meetings.ts
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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread microsoft-teams/wrangler.toml Outdated
Comment thread microsoft-teams/package.json Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread microsoft-teams/server/lib/logger.ts
@IgorFigueiredo28 IgorFigueiredo28 force-pushed the igorfigueiredo/teams-mcp branch 2 times, most recently from 07ddc41 to 751f660 Compare May 21, 2026 15:54
- 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>
@IgorFigueiredo28 IgorFigueiredo28 force-pushed the igorfigueiredo/teams-mcp branch from 751f660 to 14c0725 Compare May 21, 2026 15:59
@viktormarinho viktormarinho merged commit b68101b into decocms:main May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants