@juvantlabs/m365-graph-mcp-server
v0.2.1
Published
Microsoft Graph MCP server — OneDrive, SharePoint, and Calendar read+write for Juvant OS agents.
Downloads
167
Maintainers
Readme
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 in v0.2.0
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 toget_transcript.m365-graph:get_transcript— fetches the transcript content, strips VTT timing markers, and returns clean readable text (capped at 30 000 chars).
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.
Status
Published. v0.2.0 on npm
(@juvantlabs/m365-graph-mcp-server).
19 tools across files (OneDrive + SharePoint), Outlook Calendar, and
Teams meeting transcripts. 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-serverRequires 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>. |
CI enforces that every variable documented in this section is actually read from
process.env.<NAME>somewhere insrc/— 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/[email protected]",
"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 / 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) | { window, count, events: [] } with id / subject / start / end / location / organizer / attendees / web_url. | 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. | event_id (required) | event summary + body / body_content_type / body_truncated / recurrence. | 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_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 capped at 30 000 chars. | meeting_id + transcript_id (both required, from list_meeting_transcripts) | { meeting_id, transcript_id, char_count, 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 + 4 write on files, 5 read + 4 write on calendars, 2 read on meeting transcripts — 19 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 setupThis 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 devListens 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
[email protected].
License
MIT. Copyright (c) 2026 Juvant Srls.
