Azure client secrets lapse silently into opaque 401s (AADSTS7000222). Add a self-reported expiry date (SP_SECRET_EXPIRES=YYYY-MM-DD) and a shared secretExpiryStatus() helper in graph.mjs; surface warnings <30 days out via the CLI (stderr on every command + test output), MCP server startup log, /health, and the sharepoint_test tool. Documented in both .env.examples, setup.md, and READMEs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.1 KiB
Claude-SharepointLists
Connect Claude to Microsoft SharePoint Lists via the Microsoft Graph API, with full read/write (CRUD) over list items using app-only (client-credentials) authentication.
The repo ships the same capability in two forms, sharing one core Graph client
(graph.mjs):
| Form | Best for | Entry point |
|---|---|---|
| Skill | Claude Code on your own machine | .claude/skills/sharepoint-lists/ |
| MCP server | Claude Desktop, or self-hosted in Docker | mcp-server/ |
Repo layout
.
├── .claude/skills/sharepoint-lists/ # the skill (Claude Code)
│ ├── SKILL.md # how the skill works + usage
│ ├── .env.example # credential template (.env is git-ignored)
│ ├── scripts/
│ │ ├── sp.mjs # CLI: test|lists|columns|items|get|create|update|delete
│ │ └── lib/graph.mjs # shared, dependency-free Graph client (single source of truth)
│ └── references/
│ ├── setup.md # Azure AD (Entra) app registration walkthrough
│ └── graph-api.md # field-type formats + OData query reference
├── mcp-server/ # the MCP server (imports the same graph.mjs)
│ ├── index.mjs # stdio + Streamable HTTP transports
│ ├── Dockerfile
│ ├── .env.example
│ └── README.md # full MCP server + hosting docs
├── docker-compose.yml # run the MCP server as a container
└── .mcp.json # registers the server for Claude Code
Prerequisites (one-time)
An Entra (Azure AD) app registration with an application Graph permission
(Sites.Selected preferred over Sites.ReadWrite.All) and admin consent. The
full walkthrough is in
references/setup.md. You
end up with three secrets: SP_TENANT_ID, SP_CLIENT_ID, SP_CLIENT_SECRET.
Credentials live only in git-ignored .env files — never commit secrets.
Option A — the skill (Claude Code)
cp .claude/skills/sharepoint-lists/.env.example .claude/skills/sharepoint-lists/.env
# fill in SP_TENANT_ID / SP_CLIENT_ID / SP_CLIENT_SECRET (optionally SP_SITE_URL)
cd .claude/skills/sharepoint-lists/scripts
node sp.mjs test --site "https://<tenant>.sharepoint.com/sites/<SiteName>"
node sp.mjs items --site "<SITE_URL>" --list "Projects" --filter "fields/Status eq 'Open'"
See SKILL.md for the full command set.
Option B — the MCP server
Exposes the Lists as MCP tools. The Graph/auth logic is the same graph.mjs the
skill uses, so there is one source of truth.
Tools
| Tool | Access | Purpose |
|---|---|---|
sharepoint_test |
read | Verify credentials; with a site, list its lists |
sharepoint_list_lists |
read | Enumerate lists in a site |
sharepoint_get_columns |
read | Column internal names + types (call before writing) |
sharepoint_list_items |
read | Query items (filter/select/orderby/top/all) |
sharepoint_get_item |
read | Get one item by id |
sharepoint_create_item |
write | Create an item |
sharepoint_update_item |
write | Update an item (partial) |
sharepoint_delete_item |
write | Delete an item (irreversible) |
Set SP_READONLY=true to register only the read tools.
Transports
| Transport | Use | How the client connects |
|---|---|---|
| stdio (default) | client and server on the same machine | the app launches node index.mjs |
Streamable HTTP (--http) |
server hosted elsewhere (e.g. Docker) | client → POST /mcp with a bearer token |
stdio can't cross machines (the client launches the server as a child process), so a containerized/remote deployment must use the HTTP transport.
Environment
| Var | Required | Notes |
|---|---|---|
SP_TENANT_ID / SP_CLIENT_ID / SP_CLIENT_SECRET |
yes | from the app registration |
SP_SITE_URL |
no | default site so tools can omit the site argument |
SP_SECRET_EXPIRES |
no | client secret expiry (YYYY-MM-DD); warns via CLI//health/sharepoint_test when <30 days remain |
SP_READONLY |
no | true → read-only tool set |
MCP_AUTH_TOKEN |
HTTP only | shared bearer token required on /mcp; empty = unauthenticated |
PORT |
HTTP only | listen port (default 3838) |
Run locally
cd mcp-server
npm install
cp .env.example .env # fill in credentials
npm start # stdio
npm run start:http # Streamable HTTP on :3838
Host it in Docker (Compose)
Run from the repo root (the build context needs the shared graph.mjs):
# 1) create mcp-server/.env with SP_* creds + a strong token:
echo "MCP_AUTH_TOKEN=$(openssl rand -hex 32)" >> mcp-server/.env
# 2) build + start:
docker compose up -d --build
# 3) check:
curl -s http://localhost:3838/health # -> {"ok":true,...}
The service has restart: unless-stopped and a healthcheck. The port publishes on
all host interfaces; restrict it to a LAN IP in docker-compose.yml (or via
firewall) if desired — the bearer token guards /mcp regardless. /health is
intentionally open (liveness only, returns no data).
Full hosting details: mcp-server/README.md.
Connect a client
Claude Code — the repo-root .mcp.json registers the local stdio
server automatically.
Claude Desktop → local stdio (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"sharepoint-lists": {
"command": "/opt/homebrew/bin/node",
"args": ["/absolute/path/to/Claude-SharepointLists/mcp-server/index.mjs"]
}
}
}
Claude Desktop → remote container (bridges local stdio to the HTTP server with
mcp-remote):
{
"mcpServers": {
"sharepoint-lists": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"http://<host>:3838/mcp",
"--allow-http",
"--header", "Authorization: Bearer ${SP_MCP_TOKEN}"
],
"env": { "SP_MCP_TOKEN": "<your MCP_AUTH_TOKEN>" }
}
}
}
--allow-http permits a plain-HTTP LAN URL; the token must match the container's
MCP_AUTH_TOKEN. Fully quit and reopen Claude Desktop after editing the config.
Security notes
- App-only auth has no per-user check — the server acts as the app and can
reach every granted SharePoint site. Anyone who can call an unauthenticated
/mcpgets that access, so always setMCP_AUTH_TOKENfor the HTTP transport and keep the endpoint on a trusted network (this is not meant for public web chat). - Secrets (
SP_CLIENT_SECRET,MCP_AUTH_TOKEN) live only in git-ignored.envfiles and are never baked into the Docker image. - Field formats for writes (person/lookup/choice/date) are documented in
references/graph-api.md.