# Setup: Azure AD app registration for app-only SharePoint access This skill authenticates as an **application** (no user sign-in) using the OAuth2 client-credentials flow. You create an Entra (Azure AD) app registration once, grant it permission to SharePoint, and hand the skill three secrets. You need an Entra **admin** (or someone who is) to grant admin consent and, for the least-privilege option, to grant the app access to specific sites. ## 1. Register the application 1. Go to → **Identity** → **Applications** → **App registrations** → **New registration**. 2. Name it something recognizable, e.g. `claude-sharepoint-lists`. 3. Supported account types: **Accounts in this organizational directory only** (single tenant) is correct for app-only. 4. Leave **Redirect URI** blank — client-credentials doesn't use one. 5. **Register**. On the app's **Overview** page, copy: - **Application (client) ID** → `SP_CLIENT_ID` - **Directory (tenant) ID** → `SP_TENANT_ID` ## 2. Create a client secret 1. App → **Certificates & secrets** → **Client secrets** → **New client secret**. 2. Set an expiry (e.g. 6–12 months — note when it expires; you'll have to rotate). 3. Copy the secret **Value** immediately (it's only shown once) → `SP_CLIENT_SECRET`. 4. Record the expiry date in `.env` as `SP_SECRET_EXPIRES=YYYY-MM-DD` — the CLI and MCP server use it to warn you 30 days ahead instead of failing cold with a 401 (AADSTS7000222) when the secret lapses. > Certificates are more secure than secrets for production. This skill uses a > secret for simplicity; swapping to a certificate is a future enhancement. ## 3. Grant a Microsoft Graph application permission App → **API permissions** → **Add a permission** → **Microsoft Graph** → **Application permissions**. Pick **one** of: | Permission | Scope | Use when | |---|---|---| | **`Sites.Selected`** | Only sites an admin explicitly grants (see step 4) | **Preferred.** Least privilege — the app can touch only the sites you allow. | | `Sites.ReadWrite.All` | Read/write items in **all** site collections | Simpler, but broad. Use only if `Sites.Selected` isn't workable. | Then click **Grant admin consent for <tenant>** and confirm the status shows a green check. Without consent, every call returns `401`/`403`. ## 4. (Only for `Sites.Selected`) Grant the app access to a site `Sites.Selected` grants nothing until an admin authorizes the app on each site. Do this per site you want the skill to use. Two ways: **Option A — PnP PowerShell (simplest for admins):** ```powershell Install-Module PnP.PowerShell -Scope CurrentUser # once Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/Marketing" -Interactive Grant-PnPAzureADAppSitePermission ` -AppId "" ` -DisplayName "claude-sharepoint-lists" ` -Site "https://contoso.sharepoint.com/sites/Marketing" ` -Permissions Write # Read | Write | Manage | FullControl ``` **Option B — Microsoft Graph REST** (e.g. via Graph Explorer signed in as an admin with `Sites.FullControl.All` delegated). First get the site id, then grant: ```http GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/Marketing POST https://graph.microsoft.com/v1.0/sites/{siteId}/permissions Content-Type: application/json { "roles": ["write"], "grantedToIdentities": [ { "application": { "id": "", "displayName": "claude-sharepoint-lists" } } ] } ``` Use `read` instead of `write` for read-only access to that site. ## 5. Give the skill the credentials Copy `.env.example` to `.env` in the skill directory and fill in the three values (and optionally `SP_SITE_URL`). The CLI loads `.env` automatically, and `.env` is git-ignored so secrets aren't committed. Alternatively, export them as environment variables. ## 6. Verify ```bash node scripts/sp.mjs test --site "https://contoso.sharepoint.com/sites/Marketing" ``` Expected: `{ "ok": true, "siteId": "...", "listCount": N, "lists": [...] }`. Troubleshooting: - **Token request failed (401)** — wrong tenant id, client id, or secret (or the secret expired). Re-copy from the portal. - **403 accessDenied** — admin consent not granted (step 3), or with `Sites.Selected` the site wasn't granted to the app (step 4). - **Works for `test` (no site) but 403 with `--site`** — classic `Sites.Selected` signal: token is fine, site grant is missing. --- ## Note: graduating to an MCP server When you want this available across all sessions as native tools rather than a CLI, build a small MCP server that imports `scripts/lib/graph.mjs`: ```js import { SharePointListsClient } from "./graph.mjs"; const client = new SharePointListsClient({ tenantId: process.env.SP_TENANT_ID, clientId: process.env.SP_CLIENT_ID, clientSecret: process.env.SP_CLIENT_SECRET, }); // Expose client.listItems / getItem / createItem / updateItem / deleteItem // as MCP tools. The Graph + auth logic is already done. ``` The client is dependency-free and stateless apart from an in-memory token cache, so it drops straight into an MCP server (`@modelcontextprotocol/sdk`) or any other host.