sharepoint
Read and write SharePoint sites, document libraries, files and lists through Microsoft Graph. Use when the user asks about SharePoint sites, team documents, shared files, or SharePoint lists.
SKILL.md_extensions/connectors/skills/sharepoint/SKILL.md
GitHubSharePoint (connected)
App-only access through Microsoft Graph. Credentials in $SHAREPOINT_TENANT_ID, $SHAREPOINT_CLIENT_ID and
$SHAREPOINT_CLIENT_SECRET; $SHAREPOINT_HOSTNAME may hold the tenant's host (contoso.sharepoint.com).
There is no user behind this token: it is the app itself, so "my files" / OneDrive personal drives are out of
reach and /me always fails. A person's own Outlook mail, calendar, OneDrive and the libraries they can open belong
to the Microsoft 365 card (m365), signed in as that person; never move a token between the two cards. Get an
hour-long token, then call Graph with it:
sp_token() { curl -s -X POST "$SHAREPOINT_LOGIN_URL/$SHAREPOINT_TENANT_ID/oauth2/v2.0/token" \
-d "client_id=$SHAREPOINT_CLIENT_ID" -d "client_secret=$SHAREPOINT_CLIENT_SECRET" \
-d "scope=https://graph.microsoft.com/.default" -d "grant_type=client_credentials" | jq -r '.access_token'; }
SP=$(sp_token) # re-run when calls start answering 401 (the token lasts ~1 hour)
sp() { local m="$1" p="$2"; shift 2; curl -s -X "$m" -H "Authorization: Bearer $SP" \
-H "Content-Type: application/json" "https://graph.microsoft.com/v1.0/$p" "$@"; }
If sp_token answers no token, print the body: it comes back as {error, error_description} and the
description spells the cause out in words, AADSTS… code and all: quote it rather than guessing from the short
error alone. A 403 on a call that looks right is almost always the
missing piece from setup: admin consent was never granted, or the app has Sites.Selected and nobody granted it
this site. Say that rather than "the site does not exist".
- Find sites:
sp GET "sites?search=<TERM>" | jq -c '.value[] | {id, name, webUrl}'(root site:sp GET sites/root) - A site by URL path:
sp GET "sites/$SHAREPOINT_HOSTNAME:/sites/<SITE>" | jq '{id, displayName, webUrl}' - Subsites:
sp GET "sites/<SITE_ID>/sites" | jq -c '.value[] | {id, name, webUrl}' - Document libraries:
sp GET "sites/<SITE_ID>/drives" | jq -c '.value[] | {id, name, webUrl}', the default one issp GET "sites/<SITE_ID>/drive" - Browse a library:
sp GET "drives/<DRIVE_ID>/root/children" | jq -c '.value[] | {id, name, size, folder: (.folder != null), lastModifiedDateTime}'· a subfolder:sp GET "drives/<DRIVE_ID>/root:/<FOLDER PATH>:/children" - Find files in a library:
sp GET "drives/<DRIVE_ID>/root/search(q='<TERM>')" | jq -c '.value[] | {id, name, webUrl}' - Download a file:
curl -sL -H "Authorization: Bearer $SP" "https://graph.microsoft.com/v1.0/drives/<DRIVE_ID>/items/<ITEM_ID>/content" -o <FILE>,-L, because Graph redirects to storage - Upload a file (under 4 MB):
curl -s -X PUT -H "Authorization: Bearer $SP" --data-binary @<FILE> "https://graph.microsoft.com/v1.0/drives/<DRIVE_ID>/root:/<FOLDER>/<NAME>:/content" | jq '{id, name, webUrl}', larger files need an upload session (.../createUploadSession) - Create a folder:
sp POST "drives/<DRIVE_ID>/root/children" -d '{"name":"<NAME>","folder":{},"@microsoft.graph.conflictBehavior":"rename"}' - Lists on a site:
sp GET "sites/<SITE_ID>/lists" | jq -c '.value[] | {id, name, template: .list.template}',documentLibrarytemplates are the libraries above, seen from the list side - A list's columns:
sp GET "sites/<SITE_ID>/lists/<LIST_ID>/columns" | jq -c '.value[] | {name, displayName, readOnly}' - List rows:
sp GET "sites/<SITE_ID>/lists/<LIST_ID>/items?expand=fields&\$top=100" | jq -c '.value[] | {id, fields}', withoutexpand=fieldsthe rows come back empty of content - Add a row:
sp POST "sites/<SITE_ID>/lists/<LIST_ID>/items" -d '{"fields":{"Title":"...","Status":"Open"}}', keys are the columns' internalnames, not their display names - Update / delete a row:
sp PATCH "sites/<SITE_ID>/lists/<LIST_ID>/items/<ITEM_ID>/fields" -d '{"Status":"Done"}'·sp DELETE "sites/<SITE_ID>/lists/<LIST_ID>/items/<ITEM_ID>"
Notes: a site id is a comma-joined triple (contoso.sharepoint.com,<guid>,<guid>), ugly but ordinary, pass it
whole. Escape $ in Graph's own query params inside double quotes (\$top, \$select, \$filter) or the shell
eats them. Page with .["@odata.nextLink"], which is a full URL: call it directly with the bearer header.
Errors arrive as {error: {code, message}}; quote the message. Writes land as the app, not as a person, and
show in version history as such: say what you are about to change before changing shared documents.