Person/group columns store a numeric LookupId into each site's hidden User Information List, and SPO's ensureUser needs certificate auth that client secrets can't provide — so resolve by querying the hidden list directly via Graph (item id IS the LookupId). New resolvePersonByEmail() in graph.mjs (server-side EMail/UserName filter with a paged case-insensitive scan fallback), a 'person' CLI command, and a sharepoint_resolve_person MCP tool (9 tools now). Verified live against known ground truth (LookupIds 53 and 136) plus the not-found path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
name, description
| name | description |
|---|---|
| sharepoint-lists | Read and write Microsoft SharePoint Lists via the Microsoft Graph API using app-only (client-credentials) authentication. Use this skill whenever the user wants to query, search, list, create, add, update, edit, or delete items in a SharePoint List, inspect a list's columns, or enumerate the lists in a SharePoint site — even if they just refer to "our SharePoint", "the list", "the tracker", or a specific list/site name rather than saying "SharePoint List" explicitly. Also use it for bulk operations over list items (export, reporting, batch updates) and for connectivity/credential troubleshooting against SharePoint. This is for SharePoint *Lists* (structured rows/columns), not document libraries or files — for documents use the SharePoint search connector instead. |
SharePoint Lists
Connect to Microsoft SharePoint Lists and perform full CRUD (create, read, update, delete) on list items through the Microsoft Graph API. Authentication is app-only (OAuth2 client credentials) — no interactive login.
All operations go through one CLI, scripts/sp.mjs, which wraps the reusable
core client in scripts/lib/graph.mjs. Run it with Node (already available):
node scripts/sp.mjs <command> [flags]
Paths above are relative to this skill's directory. From elsewhere, use the absolute path, e.g.
node "<skill-dir>/scripts/sp.mjs" ....
Prerequisites (one-time)
The skill needs three credentials from an Azure AD (Entra) app registration with an application permission for SharePoint and admin consent granted:
SP_TENANT_ID— Directory (tenant) IDSP_CLIENT_ID— Application (client) IDSP_CLIENT_SECRET— a client secret value
Set them as environment variables, or copy .env.example to .env in this
skill's directory and fill it in (the CLI loads .env automatically; .env is
git-ignored). Optionally set SP_SITE_URL to a default site so --site can be
omitted.
If credentials are not yet set up, do not guess — point the user to
references/setup.md, which is a step-by-step walkthrough
of creating the app registration, choosing least-privilege permissions
(Sites.Selected is strongly preferred over Sites.ReadWrite.All), and granting
consent. Then come back and run test.
First step in any task: verify connectivity
Before doing real work, confirm auth works. This fails fast with a clear message if credentials or permissions are wrong:
node scripts/sp.mjs test --site "https://contoso.sharepoint.com/sites/Marketing"
A successful response includes the resolved site id and the names of the lists in that site.
Identifying a site and a list
- Site is given by URL:
https://{tenant}.sharepoint.com/sites/{SiteName}. The client resolves it to a Graph site id for you. A bare hostname means the root site. - List can be given by display name (e.g.
"Project Tracker"), internal name, or GUID — the client resolves names to ids and, on a typo, lists the available names so you can correct it.
When unsure what lists or columns exist, discover first rather than guessing:
node scripts/sp.mjs lists --site "<SITE_URL>"
node scripts/sp.mjs columns --site "<SITE_URL>" --list "Project Tracker"
columns is important before writing: SharePoint internal column names
often differ from their display names (a column shown as "Due Date" may be
DueDate or even OData__x0044_ue...). Always write to internal names. See
references/graph-api.md for field-type formats
(person, lookup, choice, date, etc.) and OData query details.
To write a person column, first resolve the person's email to the numeric
LookupId the column stores, then write <Column>LookupId:
node scripts/sp.mjs person --site "<SITE_URL>" --email user@domain.com
# → {"lookupId": 53, ...}; then: --fields '{"ProjectManagerLookupId": 53}'
Reading items
# Everything (follows pagination):
node scripts/sp.mjs items --site "<SITE_URL>" --list "Project Tracker" --all
# Filtered + sorted + projected:
node scripts/sp.mjs items --site "<SITE_URL>" --list "Project Tracker" \
--filter "fields/Status eq 'Open'" \
--select "Title,Status,AssignedTo" \
--orderby "fields/DueDate asc" --top 50
# One item by id:
node scripts/sp.mjs get --site "<SITE_URL>" --list "Project Tracker" --id 42
Items come back simplified as { id, webUrl, createdDateTime, lastModifiedDateTime, fields }.
Writing items
--fields takes a JSON object of internal column name → value. It also accepts
@file.json or - (read JSON from stdin), which is cleaner for large or
quote-heavy payloads.
# Create
node scripts/sp.mjs create --site "<SITE_URL>" --list "Project Tracker" \
--fields '{"Title":"Migrate database","Status":"Open","Priority":2}'
# Update (partial — only the fields you pass change)
node scripts/sp.mjs update --site "<SITE_URL>" --list "Project Tracker" \
--id 42 --fields '{"Status":"Done"}'
# Delete
node scripts/sp.mjs delete --site "<SITE_URL>" --list "Project Tracker" --id 42
Deletes are irreversible (items go to the site Recycle Bin). Confirm the item
id with the user before deleting, and prefer showing them the item via get
first. For bulk deletes, confirm the full set once, then proceed.
Bulk operations
For exports or batch edits, fetch with --all, transform the JSON however the
task needs (jq, a quick script), then loop the relevant create/update/
delete calls. Keep concurrency modest — Graph throttles with HTTP 429; the
client already retries with backoff, but don't fan out hundreds of parallel
writes.
Handling errors
The CLI prints a JSON error to stderr and exits non-zero. Common cases:
401/ token failure → bad client id/secret or tenant; re-check.env.403(accessDenied) → the app lacks permission on this site. WithSites.Selected, the site must be explicitly granted to the app — see references/setup.md.404on a list → wrong name; runliststo see valid names.400on write → usually a wrong internal field name or value format; runcolumnsand check references/graph-api.md.
Extending to an MCP server
The core logic is isolated in scripts/lib/graph.mjs (the
SharePointListsClient class, zero dependencies). To graduate this into a
standalone MCP server later, import that class and expose its methods
(listItems, getItem, createItem, updateItem, deleteItem, etc.) as MCP
tools — no rewrite of the Graph logic needed. See the note at the bottom of
references/setup.md.