@emin-bit/pnp-mcp
v1.2.0
Published
MCP (Model Context Protocol) server bridging Claude with the PnP PowerShell module for SharePoint Online and Microsoft 365 administration. Wraps `pwsh` 7+ with PnP.PowerShell module installed; exposes 91 typed tools (auth lifecycle, sites/webs/lists/files
Maintainers
Readme
@emin-bit/pnp-mcp
Model Context Protocol server bridging Claude with the PnP PowerShell module for SharePoint Online and Microsoft 365 administration.
Wraps a long-lived pwsh 7+ REPL session that has the PnP.PowerShell module loaded, then exposes auth lifecycle, sites, webs, lists, items, files, content types, fields, permissions, provisioning templates, modern pages, hub sites, M365 groups, and navigation as 90 typed MCP tools — plus a generic pnp_run passthrough for anything else.
Works on macOS, Linux, and Windows. Cross-platform pwsh discovery, auth state preserved across calls, safe-mode gating for destructive operations, background jobs for long-running provisioning.
Why this exists
PnP PowerShell is the most complete tooling for SharePoint Online + M365 admin work, but it has ~700 cmdlets. Asking an LLM to drive raw PowerShell over a generic shell tool means: cold-start cost on every call, no auth state preservation, no safety net for Remove-PnPSite, no schema for the LLM to reason about parameters.
This server fixes all of that:
- One pwsh REPL, kept alive for the session. Auth lives in the process —
Connect-PnPOnlineonce, run dozens of cmdlets without re-authenticating. - Typed Zod schemas for the 89 most-used operations, so Claude picks parameters correctly without consulting cmdlet help.
- Safe-mode gating. Destructive operations (
Remove-*,Clear-*,New-PnPSite,Invoke-PnPTenantTemplate, etc.) require an explicitconfirm: true. The check walks every cmdlet in pipelines and multi-statement commands. - Background jobs. Provisioning a TeamSite or applying a PnP template can take minutes — Claude Desktop's MCP transport has a ~60s timeout. Background jobs run in separate pwsh processes; track via
job_status/job_wait. - Live-enum verification in CI. Every hardcoded enum string in TypeScript is checked against the live PnP module on each release via
npm run verify-enums.
Requirements
- Node.js ≥ 18 (the MCP server)
- PowerShell 7+ (
pwshon PATH) — Windows PowerShell 5.1 is not supported - PnP.PowerShell module — install via the bundled
setup_install_pnp_moduletool, or manually:Install-Module PnP.PowerShell -Scope CurrentUser
The preflight MCP tool diagnoses all of the above and reports what's missing.
Installation
Global (recommended for Claude Desktop)
npm install -g @emin-bit/pnp-mcpThis installs a pnp-mcp binary on PATH.
Per-project
npm install @emin-bit/pnp-mcpClaude Desktop configuration
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) and add:
{
"mcpServers": {
"pnp": {
"command": "pnp-mcp"
}
}
}Or with npx (no global install needed):
{
"mcpServers": {
"pnp": {
"command": "npx",
"args": ["-y", "@emin-bit/pnp-mcp"]
}
}
}Restart Claude Desktop, and the 90 PnP tools become available.
Auto-update notifications
The server checks the npm registry at most once per 24 hours (background, cached). When a newer version is found, the next launch prints a plain-text banner to stderr with the upgrade command. Set "env": { "PNP_MCP_DISABLE_UPDATE_CHECK": "1" } in the MCP entry to silence.
For users who'd rather have updates pulled automatically (with a 1–3 second cold-start cost), use the npx ... @latest form:
{
"mcpServers": {
"pnp": {
"command": "npx",
"args": ["-y", "@emin-bit/pnp-mcp@latest"]
}
}
}npx will check for and install a newer version on every Claude Desktop launch. Note: a server restart is still required to load the new code, but Claude Desktop already restarts MCP servers on each app start.
Disabling safe-mode (NOT recommended)
Destructive cmdlets are gated behind confirm: true by default. If you really want to skip the gate (e.g. for fully automated pipelines), set:
{
"mcpServers": {
"pnp": {
"command": "pnp-mcp",
"env": { "PNP_MCP_SAFE_MODE": "off" }
}
}
}Treat this like rm -rf / — there is no undo for Remove-PnPSite -Force.
Quickstart
After Claude Desktop reconnects, ask Claude something like:
Run preflight to check my PnP setup.
Claude calls preflight → reports Node, pwsh, module, auth status. If the module is missing:
Install the PnP module.
Claude calls setup_install_pnp_module (with confirm: true). Then connect:
Connect interactively to https://contoso.sharepoint.com/sites/marketing.
Claude picks pnp_auth_connect_interactive (browser pop-up). Then:
What lists are on this site?
Claude calls pnp_list_list.
Create a list called "Project Tracker" with a Title and a DueDate field.
Claude calls pnp_list_new (with confirm: true), then pnp_field_add for the date column.
For more end-to-end flows, see examples/.
Tool catalog
90 tools across 5 phases. The server's MCP instructions text (sent at handshake) is the canonical guide for the LLM; this list is for humans browsing the README.
preflight— diagnose Node, pwsh, PnP module, auth statesetup_install_pnp_module— install PnP.PowerShell viaInstall-Modulepnp_auth_connect_interactive— browser pop-uppnp_auth_connect_device_code— URL + code flow (headless)pnp_auth_connect_sp_secret— service principal + secretpnp_auth_connect_sp_cert— service principal + .pfx certificatepnp_auth_connect_managed_identity— Azure resource MI (with IMDS pre-check)pnp_auth_disconnect— clear the session's PnP connectionpnp_session_status— show current connection (URL, account, scopes)job_list,job_status,job_wait,job_cancel— manage background jobs
pnp_site_list,pnp_site_get,pnp_site_get_by_urlpnp_site_new(TeamSite / CommunicationSite / TeamSiteWithoutMicrosoft365Group; defaultsbackground: true)pnp_site_remove(defaultsbackground: true)pnp_web_list,pnp_web_get,pnp_web_new,pnp_web_removepnp_tenant_get,pnp_tenant_set
pnp_list_list,pnp_list_get,pnp_list_new,pnp_list_set,pnp_list_removepnp_listitem_list,pnp_listitem_get,pnp_listitem_add,pnp_listitem_set,pnp_listitem_removepnp_view_list,pnp_view_add,pnp_view_removepnp_file_get,pnp_file_add,pnp_file_copy,pnp_file_move,pnp_file_removepnp_folder_get,pnp_folder_add,pnp_folder_remove
pnp_contenttype_list,pnp_contenttype_get,pnp_contenttype_add,pnp_contenttype_set,pnp_contenttype_removepnp_field_list,pnp_field_get,pnp_field_add,pnp_field_set,pnp_field_remove(22-valueFieldTypeenum)pnp_group_list,pnp_group_get,pnp_group_new,pnp_group_set,pnp_group_removepnp_role_definition_list,pnp_role_set_web,pnp_role_set_list,pnp_role_set_listitempnp_template_get(export PnP XML),pnp_template_apply(defaultsbackground: true)
pnp_page_list,pnp_page_get,pnp_page_add,pnp_page_set,pnp_page_remove(LayoutType incl.Dashboard/NewsDigest;PromoteAsusesNewsArticle)pnp_hubsite_list,pnp_hubsite_register,pnp_hubsite_set,pnp_hubsite_associate,pnp_hubsite_disassociatepnp_m365group_list,pnp_m365group_get,pnp_m365group_new,pnp_m365group_set,pnp_m365group_removepnp_m365group_member_add,pnp_m365group_member_remove,pnp_m365group_owner_add,pnp_m365group_owner_removepnp_navigation_list,pnp_navigation_add,pnp_navigation_remove(all: truerequireslocation)
pnp_run— execute any PowerShell expression in the live session (gated by safe-mode)pnp_help— list/describe PnP cmdlets (Get-Help/Get-Command -Module PnP.PowerShell)
Safety model
The isDestructive() check (see src/safety.ts) inspects each command for:
- Destructive verbs —
Remove,Clear,Reset,Disable,Stop,Disconnect,Revoke,Deny,Block,Uninstall,Unpublish. - Explicit destructive cmdlets — list of ~56 cmdlets that aren't covered by the verb rule but mutate tenant/site state (
New-PnPSite,Invoke-PnPTenantTemplate,Set-PnPListItem,Add-PnPField,Register-PnPHubSite,Add-PnPSiteCollectionAdmin, …). - Dangerous parameters —
-Force,-Confirm:$false,-IgnoreOnPremError.
The check walks every Verb-Noun token after stripping comments and string literals — so Get-PnPListItem | Remove-PnPListItem and Connect-PnPOnline ...; Remove-PnPSite ... are both gated, not just the first cmdlet.
When safe-mode blocks a command, the tool returns isError: true with a BLOCKED: message explaining why, so Claude can decide to retry with confirm: true after reading pnp_session_status.
Development
git clone https://github.com/Emin-bit/pnp-mcp.git
cd pnp-mcp
npm install
npm run build # tsc → dist/
npm test # 72 smoke tests against a live pwsh session
npm run verify-enums # check every TS enum against live PnP enumsThe smoke test is a real MCP-protocol round-trip: it spawns the built server, sends initialize + tools/list + 70+ tools/call requests, and asserts safety gating, error handling, JSON round-trip integrity, and live-enum coverage. It requires pwsh and PnP.PowerShell to be installed (preflight will tell you what's missing).
Architecture notes
src/pwsh.ts— long-lived REPL session manager. Each tool call sends a base64-encoded user command wrapped in a marker-protocol try/catch, then reads stdout until the END marker. ANSI sequences are stripped before parsing.src/safety.ts— destructive-command detection (string-aware, comment-aware, pipeline-aware).src/runner.ts— the commonrunAsTool({ toolName, command, timeoutMs })wrapper used by every typed tool.src/jobs.ts— background job tracking (separate pwsh per job, no auth inheritance).src/tools/*.ts— one file per tool family (auth, site, web, list, listitem, view, file, folder, contenttype, field, permission, template, page, hubsite, m365group, navigation, preflight, jobs).verify-enums.mjs— CI script. Author convention: prefix everyz.enum([…])with// @verify-enum [DotNetTypeName]and the script will assert each TS literal exists in the live .NET enum.
Companion project
This is a sibling to @emin-bit/power-platform-mcp, a separately-installable MCP server for the Power Platform CLI (pac / pacx). They're independent — install whichever (or both) you need.
License
MIT — see LICENSE.
Contributing
Issues and PRs welcome at github.com/Emin-bit/pnp-mcp. For new tools, please:
- Add a Zod schema with descriptive
.describe()text on every parameter. - Add a
// @verify-enum [DotNetType]marker if you're adding a new enum. - Add a smoke test in
smoke-test.mjs(at minimum: confirm gating works for destructive operations). - Run
npm run verify-enumsandnpm test— both must pass.
