Skip to main content
Glama
juvantlabs
by juvantlabs

M365 Graph MCP Server

@juvantlabs/m365-graph-mcp-server — Model Context Protocol server wrapping the Microsoft Graph API for OneDrive, SharePoint, and Calendar (read + write). Designed to be consumed by Juvant OS agents (or any MCP-aware client) via npx.

Fulfills the m365-graph role per docs/adr/0002-mcp-abstract-roles.md in the handbook (concrete-only — Microsoft Graph is the single provider). The scope of this server (files + calendar; no mail send, no Teams chat) follows the threat-model boundary rule in docs/adr/0003-mcp-server-scope-boundaries.md: mail and Teams chat have materially different blast radius and would ship as separate <vendor>-<capability>-mcp-server packages if a real Juvant OS need surfaces; outbound Teams notifications go through webhooks (Adaptive Cards), not MCP. Per-company instance config binds this server in .juvant/config.json.

What's new

Teams meeting transcript support. Two new read-only tools:

  • m365-graph:list_meeting_transcripts — given a calendar event ID, lists available post-meeting transcripts. Returns transcript IDs to pass to get_transcript.

  • m365-graph:get_transcript — fetches the transcript content, strips VTT timing markers, and returns clean readable text.

Two new delegated scopes required (admin consent — see § Tools): OnlineMeetings.Read and OnlineMeetingTranscript.Read.All. Re-run npm run setup after upgrading to acquire them.

See CHANGELOG.md for the full change list.

Related MCP server: Microsoft MCP

Status

Published. v0.4.0 on npm (@juvantlabs/m365-graph-mcp-server), released 2026-06-26. 19 tools across files (OneDrive + SharePoint), Outlook Calendar, and Teams meeting transcripts. v0.4.1 is prepared on main — adds create_folder (bringing the tool count to 20) and fixes the consumeConfirmation gc-ordering bug; pending manual tag + release trigger. Published via npm Trusted Publishing (OIDC-based auth from GitHub Actions; no static NPM_TOKEN) with provenance attestation; manual approval gate on the production GitHub Environment guards the publish step.

Originally generated by juvantlabs/juvant-tools scaffold mcp-server on 2026-05-03, conforming to the mcp-server.md spec. See CHANGELOG.md for the per-version history.

Install + run

# One-time OAuth (opens browser, persists tokens in OS keychain):
npx @juvantlabs/m365-graph-mcp-server setup

# Run the MCP server on stdio (default subcommand):
npx @juvantlabs/m365-graph-mcp-server

Requires Node ≥ 20. Both invocations expect the env vars below (typically loaded from .env.local via --env-file, or set by your MCP client when it spawns the server).

Environment variables

Required:

Variable

Purpose

M365_CLIENT_ID

Microsoft Entra application (client) ID for the registered app.

M365_CLIENT_SECRET

Client secret for the registered app. Stored only in the consumer's environment; never in .juvant/config.json.

M365_TENANT_ID

Microsoft Entra tenant ID (UUID). The canonical adoption pattern is single-tenant — see ARCHITECTURE.md § Tenancy model. The regex also accepts common / organizations / consumers for technical compatibility with Microsoft authority strings, but multi-tenant operation is not the supported deployment shape.

Optional:

Variable

Purpose

MCP_SERVER_LOG_LEVEL

Log level for diagnostics on stderr (default info).

M365_DOWNLOAD_DIR

Override the per-tenant sandbox directory used by download_file. Default: $XDG_CACHE_HOME/m365-graph-mcp-server/<tenant-id> or ~/.cache/m365-graph-mcp-server/<tenant-id>.

M365_TRANSCRIPT_MAX_BYTES

Max raw VTT bytes read from Graph per get_transcript call. Defense-in-depth bound against the audit-S7 whole-file-buffering pattern. Default 10000000 (10 MB ≈ 8 h+ of speech).

M365_TRANSCRIPT_MAX_CHARS

Max parsed-text characters returned in a single get_transcript response. Long transcripts page via the tool's offset + next_offset fields. Default 200000 (~2 h of speech).

CI enforces that every variable documented in this section is actually read from process.env.<NAME> somewhere in src/ — placeholder names containing <> are skipped. Documenting an env var without wiring it up will fail the build (handbook anti-pattern S2).

OAuth scope minimization is per-tool; see the tool catalog and ARCHITECTURE.md for the per-tool scope justifications.

Binding

The Juvant OS adopter binds this server in .juvant/config.json:

{
  "m365-graph": {
    "provider": "microsoft",
    "mcp_server": "npx @juvantlabs/m365-graph-mcp-server@0.4.0",
    "scope": "rw"
  }
}

Pinning the version in mcp_server keeps installs reproducible. The canonical inventory entry, with the same pin, is at juvant-os/docs/MCP_INVENTORY.md — refer to it for the matrix-side bindings (which agents have m365-graph:rw granted by default in the v0 seed) and the wizard Step 8.5 cross-check semantics.

Tools

Tool

Purpose

Input

Output

Required scope

m365-graph:list_drives

Lists the drives the user has access to (primary OneDrive + shared document libraries).

(none)

{ primary, accessible: [] } with id / driveType / name / webUrl / owner.

Files.Read

m365-graph:list_items

Lists immediate children (files + folders) of a folder. Defaults to the drive root.

drive_id?, item_id?, limit? (1–100, default 50)

{ count, items: [] } with id / name / type / size / child_count / lastModified / webUrl.

Files.Read

m365-graph:search_files

Searches files by name and content within a drive.

query (required), drive_id?, limit? (1–50, default 20)

{ count, results: [] } with id / name / path / size / is_folder / lastModified / webUrl.

Files.Read

m365-graph:download_file

Downloads a file to a per-tenant local sandbox. Returns the local path; agent reads via a filesystem-aware tool. Streams, capped at 200 MB.

item_id (required), drive_id?

{ local_path, size_bytes, name, content_type }

Files.Read

m365-graph:list_calendars

Lists the user's calendars (primary + group / shared).

limit? (1–100, default 50)

{ count, calendars: [] } with id / name / color / owner_name / owner_email / is_default / can_edit / can_share.

Calendars.Read

m365-graph:list_events

Lists events in a date window. Recurrences are expanded — each occurrence is its own event.

start + end (ISO 8601, required), calendar_id?, limit? (1–200, default 100)

{ calendar_id, window, count, events: [] } with id / subject / body_preview / start / end / is_all_day / is_online_meeting / location / organizer_name / organizer_email / attendees / webLink.

Calendars.Read

m365-graph:search_events

Searches events by subject substring (Graph $search isn't supported on Events; subject-only via contains()). Returns recurrence series masters, not occurrences.

query (required), limit? (1–50, default 20)

{ count, results: [] } (same event shape).

Calendars.Read

m365-graph:get_event

Fetches full details for a single event — body (capped at 8000 chars), attendees with response statuses, location, recurrence rule, online-meeting join URL when applicable.

event_id (required)

event summary + body / body_content_type / body_truncated / recurrence / online_meeting_join_url.

Calendars.Read

m365-graph:upload_file

Uploads a local file to a drive. Auto-routes between single PUT (≤ 4 MB) and resumable upload session (> 4 MB, 10 MB chunks). 200 MB hard cap.

local_path (required), drive_id?, parent_item_id?, name?, conflict_behavior? (fail/replace/rename, default fail)

{ uploaded: { id, name, size, webUrl, upload_path } }

Files.ReadWrite

m365-graph:create_folder

Creates a folder in a drive (OneDrive primary by default; SharePoint document library via drive_id). Synchronous POST to /children. Returns the new folder's item ID so it can be passed as parent_item_id to a subsequent upload_file / create_folder call. replace conflict behavior is intentionally not exposed (compose delete_file + create_folder if truly needed).

name (required), parent_item_id?, drive_id?, conflict_behavior? (fail/rename, default fail)

{ created: { id, name, webUrl, parent_id, child_count } }

Files.ReadWrite

m365-graph:create_event

Creates a new event on the user's primary calendar (or a specified calendar). Sends invitations to attendees by Graph default.

subject + start + end (required), timezone? (default UTC), body?, body_content_type? (text/html), location?, attendees?, is_all_day?, calendar_id?

{ created: <event summary> }

Calendars.ReadWrite

m365-graph:update_event

Updates an existing event. All fields except event_id are optional; only provided fields are PATCHed. Attendees: full replacement, not merge — pass the full intended list.

event_id (required), then any subset of subject/start+end+timezone/body+body_content_type/location/attendees/is_all_day

{ updated: <event summary> }

Calendars.ReadWrite

m365-graph:copy_file

Async copy with polling. POSTs to /items/{id}/copy, polls the monitor URL with exponential backoff (1s → 2s → … capped at 30s) until completion. Falls back to list-by-name if the monitor's completed response omits resourceLocation (common Graph quirk).

item_id + target_parent_id (required); source_drive_id?, target_drive_id?, new_name?, wait_max_seconds? (1–1800, default 300)

{ status: "completed", copied: { id, name, ... } }

Files.ReadWrite

m365-graph:move_file

Synchronous move within a drive (PATCH parentReference). Cross-drive moves are not supported here — use copy_file + delete_file for those.

item_id + target_parent_id (required); drive_id?, new_name?

{ moved: { id, name, ... } }

Files.ReadWrite

m365-graph:delete_file

Two-phase spec/approval: 1st call returns preview + confirmation_token; 2nd call (same args + token) executes the DELETE. Token single-use, 5 min expiry, tied to exact spec (canonical-JSON SHA-256).

item_id (required), drive_id?, confirmation_token?

preview { item, confirmation_token, expires_at } or execute { deleted: { ... } }

Files.ReadWrite

m365-graph:cancel_event

Two-phase like delete_file. Cancels a meeting the user organizes (sends cancellation notice to attendees).

event_id (required), comment?, confirmation_token?

preview or { cancelled: { event_id } }

Calendars.ReadWrite

m365-graph:decline_event

Two-phase. Declines an event the user is invited to (as attendee — distinct from cancel which is for events the user organizes). Sends a decline RSVP unless send_response: false.

event_id (required), comment?, send_response? (default true), confirmation_token?

preview or { declined: { event_id, send_response } }

Calendars.ReadWrite

m365-graph:search_events_content

Subject + body content search via the Microsoft Search API (POST /search/query). Distinct from search_events (subject-only via $filter). Returns recurrence series masters; for occurrences in a window use list_events.

query (required), limit? (1–50, default 25), from? (pagination offset, default 0)

{ count, total, results: [<event summary>] }

Calendars.Read

m365-graph:list_meeting_transcripts

List available transcripts for a Teams meeting identified by its calendar event ID. Transcripts are post-meeting only and require recording to have been enabled by the organizer.

event_id (required)

{ event_id, meeting_id, count, transcripts: [{ id, meeting_id, created_at, end_at }] }

Calendars.Read, OnlineMeetings.Read ¹, OnlineMeetingTranscript.Read.All ¹

m365-graph:get_transcript

Fetch the text content of a Teams meeting transcript. VTT timing markers are stripped; returns clean readable text. Long transcripts page via offset + next_offset; per-call cap and upstream byte cap configurable via M365_TRANSCRIPT_MAX_CHARS / M365_TRANSCRIPT_MAX_BYTES (defaults 200 000 chars / 10 MB).

meeting_id + transcript_id (required, from list_meeting_transcripts); offset? (0..2_000_000_000), max_chars?

{ meeting_id, transcript_id, offset, char_count, next_offset, total_char_count, truncated, vtt_truncated, transcript }

OnlineMeetingTranscript.Read.All ¹

¹ Admin consent required. OnlineMeetings.Read and OnlineMeetingTranscript.Read.All must be granted in the Entra app registration under API permissions → Add a permission → Microsoft Graph → Delegated → Grant admin consent. Without admin consent these tools return 403 Forbidden.

That's 4 read + 5 write on files, 5 read + 4 write on calendars, 2 read on meeting transcripts — 20 tools total. Read tools exercise delegated Files.Read + Calendars.Read; write tools require Files.ReadWrite + Calendars.ReadWrite; transcript tools require OnlineMeetings.Read + OnlineMeetingTranscript.Read.All (all separately granted + admin-consented in the Entra app).

Local development

The repo expects a .env.local file with your tenant's credentials. Bootstrap from the template:

cp .env.example .env.local
# then edit .env.local with your M365_TENANT_ID, M365_CLIENT_ID,
# and M365_CLIENT_SECRET — see ARCHITECTURE.md § Authentication
# for the Entra app registration flow.

.env.local is gitignored.

One-time OAuth setup

The first time you run the server, you need to complete an OAuth flow to populate the OS keychain with refresh tokens:

npm run setup

This opens your browser, signs you in to your tenant, captures the authorization code via a one-shot listener at http://localhost:3000/auth/callback, and persists the resulting tokens via @napi-rs/keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager). After that, the server uses cached tokens silently — refreshes as needed via the cached refresh grant.

Run the MCP server

npm run dev

Listens on stdio. Useful when developing alongside an MCP client like Claude Code: configure the client to spawn npm run dev (or tsx --env-file=.env.local src/index.ts) as its MCP server command.

Architecture

See ARCHITECTURE.md for design rationale: scope, OAuth model with @azure/msal-node, token persistence via @napi-rs/keyring, per-tool scope minimization, filesystem sandboxing for upload/download tools, and async-op polling for copy / move.

Contributing

See CONTRIBUTING.md. The repo follows the juvantlabs/handbook conventions for MCP server repos.

Security

See SECURITY.md for the disclosure process. Per the handbook security disclosure process, report vulnerabilities privately via GitHub Security Advisory or security@juvant.io.

License

MIT. Copyright (c) 2026 Juvant Srls.

Maintenance

ActivityStale
ResponsivenessResponsive

Related MCP Connectors

Related MCP Servers