M365 Graph MCP Server
Model Context Protocol server wrapping Microsoft Graph API for OneDrive, SharePoint, and Calendar with read and write operations, plus Teams meeting transcript support.
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-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>. |
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/m365-graph-mcp-server@0.1.3",
"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 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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。
mcp-server-qdrant
这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。