npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

superfast-m365-connector

v0.4.1

Published

Read-only MCP server for Microsoft 365 security and licensing data across GDAP customer tenants

Downloads

581

Readme

ms365-gdap

Read-only MCP server giving Claude access to Microsoft 365 security and licensing data across Superfast IT's GDAP customer tenants, for Strategic Business Review reporting.

For any customer tenant it answers: Secure Score, security defaults / conditional access posture, licence holdings and consumption, MFA registration coverage, and Intune device compliance — via seven tools, including a one-call get_tenant_security_summary.

This server never writes. It only issues GET requests to Microsoft Graph, and a global fetch guard makes any non-GET request to a Graph host throw before it leaves the process. Only read application permissions are requested.


How authentication actually works (read this first)

The runtime model is app-only (client credentials): for each customer query the server acquires a token from https://login.microsoftonline.com/{customer-tenant-id} using the partner-tenant app registration's client ID and secret, then calls Graph v1.0. Tokens are cached per tenant and refreshed 2 minutes before expiry.

Important — how the app gets into a customer tenant. The old DAP "pre-consent" pattern (adding the app's service principal to the AdminAgents / GDAP security group) does not work under GDAP. Microsoft's GDAP migration guidance is explicit that Entra roles can no longer be granted to an application through security group membership. What works instead:

  1. The app registration lives in the partner tenant, is multitenant, and declares the application permissions listed below.
  2. The app is consented into each customer tenant via the Partner Center applicationconsents API (or by the customer's own Global Administrator using the admin-consent URL). This creates the service principal in the customer tenant and grants the permissions there.
  3. From then on, client-credential tokens against that customer tenant's authority carry those application permissions. This is exactly what this server does at runtime.

Governance caveat, so we go in with eyes open: Microsoft's docs state that pure app-only consent sits outside the GDAP time-bound contract — the consent granted in a customer tenant persists until it is revoked, even if the GDAP relationship expires. Microsoft's formally recommended pattern for GDAP automation is app+user (the Secure Application Model with a refresh token). App-only with per-tenant consent is nonetheless the standard MSP pattern for unattended reporting (used by CIPP-style tooling and documented by the MSP community) and is what this server implements, because it needs no interactive account or refresh-token custody. The obligation it creates: when a customer offboards, remove the app's enterprise application from their tenant (Entra admin centre → Enterprise applications → delete), because GDAP expiry alone will not cut off access.


Application permissions and why each is needed

| Permission (Application) | Used by | Why | | --- | --- | --- | | SecurityEvents.Read.All | get_secure_score | /security/secureScores and /security/secureScoreControlProfiles | | Policy.Read.All | get_access_policies | Security defaults policy and conditional access policies | | Organization.Read.All | get_licences | /subscribedSkus | | Directory.Read.All | get_users_summary | /users with account state and licence assignments | | DeviceManagementManagedDevices.Read.All | get_device_compliance | Intune /deviceManagement/managedDevices | | AuditLog.Read.All | get_users_summary (MFA report) | /reports/authenticationMethods/userRegistrationDetails | | Channel.ReadBasic.All | get_teams_channels | Channels per team incl. standard/private/shared type | | ChannelMember.Read.All | get_teams_channels | Channel members (incl. cross-tenant), sharedWithTeams, allMembers | | Reports.Read.All | get_mailbox_usage | Mailbox/OneDrive/SharePoint usage report CSVs | | SecurityIncident.Read.All | get_defender_incidents | Defender XDR incidents | | DeviceManagementConfiguration.Read.All | get_intune_baseline | Compliance policies + configuration profiles | | DeviceManagementApps.Read.All | get_intune_apps | Published app inventory (/deviceAppManagement/mobileApps). The v1.0 docs wrongly list DeviceManagementConfiguration.Read.All as sufficient — the live API rejects it and names this scope explicitly. Verified against a real tenant, Aug 2026. | | MailboxSettings.Read | get_forwarding_rules | Per-user inbox rules (forwarding audit) | | SharePointTenantSettings.Read.All | get_sharing_audit | Tenant sharing policy (anonymous links, resharing, domain restrictions) — settings only, no content access |

Rejected permission (July 2026): Sites.Read.All was considered for site inventory and rejected — app-only, it can read the contents of every SharePoint site and OneDrive in consented tenants, which is out of proportion for a reporting server. Site inventory comes from the SharePoint usage report (Reports.Read.All, metadata only) instead. Do not add it without revisiting this decision.

Note: the original specification listed Reports.Read.All for the MFA registration report. Per the v1.0 API reference, userRegistrationDetails requires AuditLog.Read.AllReports.Read.All does not grant it. Reports.Read.All is only needed if usage reports (email activity etc.) are added later, so it is omitted for least privilege.

All endpoints used are Graph v1.0 — no beta endpoints were needed; the MFA registration report GA'd to v1.0 some time ago.


Setup

1. Create the app registration (partner tenant)

Reusing an existing registration (e.g. Rewst): the server only needs a client ID + secret whose app has the permissions above consented in each customer tenant. Reusing the Rewst app registration would work mechanically, but it is not recommended: Rewst's registration carries broad write permissions, so this read-only reporting server would hold a credential capable of far more than it needs, and secret rotation/blast radius get entangled. A dedicated registration keeps the credential's power matched to the job. The steps below assume a new, dedicated registration.

In the Superfast IT partner tenant (Entra admin centre → App registrations → New):

  1. Name: Superfast IT - ms365-gdap (read-only reporting).
  2. Supported account types: Accounts in any organizational directory (multitenant). This is required — single-tenant apps cannot be consented into customer tenants.
  3. No redirect URI needed.
  4. API permissions → Add → Microsoft Graph → Application permissions: SecurityEvents.Read.All, Policy.Read.All, Organization.Read.All, Directory.Read.All, DeviceManagementManagedDevices.Read.All, AuditLog.Read.All. Grant admin consent for the partner tenant (needed so the app also works against our own tenant and so the permission set is visible when consenting downstream).
  5. Certificates & secrets → new client secret. Record the value; set a calendar reminder before expiry (max 24 months).
  6. Record: partner tenant ID → PARTNER_TENANT_ID, application (client) ID → APP_CLIENT_ID, secret value → APP_CLIENT_SECRET.

2. GDAP relationship prerequisites

Each customer's GDAP relationship must be active, with a partner security group assigned roles. Minimum role set for this server's purposes:

| GDAP role | Why | | --- | --- | | Cloud Application Administrator (or Application Administrator) | Required by the Partner Center consent API to consent the app into the customer tenant. Only needed at consent time — if you prefer, grant it via a short-lived second relationship and remove it after consenting. | | Global Reader | Covers directory, licence, policy and Intune reads for engineers working delegated (app+user); also the sensible read-everything baseline for an SBR relationship. | | Security Reader | Secure Score and security posture reads in the Defender portal for delegated sessions. | | Intune Administrator | Community-verified requirement: the consent API validates that the relationship's roles cover the workloads of the permissions being granted, so Intune permissions need an Intune role present. (Use "Intune Service Administrator" naming in older portals.) |

The person running the consent script must be a member of AdminAgents (required for any Partner Center API call) and of the security group assigned to the customer's GDAP relationship.

Day-to-day queries do not depend on the GDAP roles — once consent is granted, the app-only token carries the application permissions directly (see the governance caveat above).

3. Consent the app into each customer tenant

Field-tested reality (July 2026): the Partner Center consent API below creates the service principal in the customer tenant but grants the permissions as delegated only — the resulting app-only token carries no roles and every Graph call 403s. This matches Microsoft's statement that explicit app-only consent isn't supported through the GDAP consent contract. The step that actually grants the application permissions is the admin-consent URL (Option B), opened interactively by an account holding Global Administrator in the customer tenant (your own account works where the GDAP relationship includes Global Administrator). Practical flow for a fleet: run scripts/consent.ps1 once if you like (it creates the SPs and surfaces GDAP problems tenant-by-tenant), then work through consent-links.html — generated per-tenant admin-consent links — which is the part that makes app-only tokens work. Verify with scripts below or npm run check.

Option A — Partner Center consent API (creates the SP; NOT sufficient on its own):

Install-Module PartnerCenter -Scope CurrentUser

$appId           = "<APP_CLIENT_ID>"
$partnerTenantId = "<PARTNER_TENANT_ID>"

# Interactive sign-in as the CSP service/admin account (honours MFA, which
# Partner Center mandates). The account needs AdminAgents + GDAP group membership.
$token = New-PartnerAccessToken -ApplicationId $appId -Tenant $partnerTenantId `
  -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -UseAuthorizationCode
Connect-PartnerCenter -AccessToken $token.AccessToken

$graphGrant = New-Object Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant
$graphGrant.EnterpriseApplicationId = '00000003-0000-0000-c000-000000000000'   # Microsoft Graph
$graphGrant.Scope = 'SecurityEvents.Read.All,Policy.Read.All,Organization.Read.All,Directory.Read.All,DeviceManagementManagedDevices.Read.All,AuditLog.Read.All'

# Loop every tenant in tenants.json
(Get-Content tenants.json | ConvertFrom-Json) | ForEach-Object {
  Write-Host "Consenting into $($_.display_name)…"
  New-PartnerCustomerApplicationConsent -CustomerId $_.tenant_id -ApplicationId $appId `
    -ApplicationGrants @($graphGrant) -DisplayName 'Superfast IT - ms365-gdap (read-only reporting)'
}

The REST equivalent, if you'd rather call it from Rewst or a script without the module:

POST https://api.partnercenter.microsoft.com/v1/customers/{customer-tenant-id}/applicationconsents
Authorization: Bearer {partner-center user token}
Content-Type: application/json

{
  "applicationId": "<APP_CLIENT_ID>",
  "displayName": "Superfast IT - ms365-gdap (read-only reporting)",
  "applicationGrants": [
    {
      "enterpriseApplicationId": "00000003-0000-0000-c000-000000000000",
      "scope": "SecurityEvents.Read.All,Policy.Read.All,Organization.Read.All,Directory.Read.All,DeviceManagementManagedDevices.Read.All,AuditLog.Read.All"
    }
  ]
}

Re-running consent after adding a permission: delete the existing consent first (Remove-PartnerCustomerApplicationConsent / DELETE …/applicationconsents/{applicationId}), then re-create it — the API does not merge grants.

Option B — admin-consent URL (required; this is what grants the app permissions): open this URL as an account with Global Administrator in the customer tenant — your own account via GDAP where the relationship includes that role, or the customer's admin otherwise:

https://login.microsoftonline.com/{customer-tenant-id}/adminconsent?client_id=<APP_CLIENT_ID>

4. Configure and verify

cd /Users/jamescash/mcp/m365-gdap
npm install && npm run build
cp .env.example .env          # fill in the three values
cp tenants.sample.json tenants.json   # fill in the real customer mapping
npm test                      # 26 unit + integration tests
npm run check -- "acme"       # live smoke test: full summary against one tenant

npm run check exits 0 only if every section succeeded, so it can gate a scheduled health check. Use list_tenants with check_connectivity: true from Claude to see which tenants still lack consent.


tenants.json

An array mapping customer tenants to our PSA/documentation IDs (see tenants.sample.json):

{
  "tenant_id": "11111111-2222-3333-4444-555555555555",
  "display_name": "Acme Manufacturing Ltd",
  "default_domain": "acmemanufacturing.co.uk",
  "halo_client_id": 42,
  "itglue_org_id": 1234567
}

Every tool accepts tenant as a tenant GUID, an exact display name or default domain, or a case-insensitive partial match on either. Ambiguous partials return an error listing the candidates.

Tools

| Tool | Returns | | --- | --- | | list_tenants | The mapping; with check_connectivity: true, per-tenant token check (failures = not yet consented) | | get_consent_status | Per-tenant permission audit from fresh token role claims: complete/partial/not consented, missing permissions, and the consent URL to fix each. One tenant or the whole estate | | get_secure_score | Latest/max score, percentage, top 10 improvement actions by potential gain with remediation text | | get_access_policies | Security defaults on/off; each CA policy with state (enabled/disabled/report-only) and a plain-English summary | | get_licences | subscribedSkus with friendly product names, prepaid/consumed/available units | | get_users_summary | Member/guest/enabled/disabled/licensed counts; MFA-registered vs not, naming unregistered enabled users | | get_user_licences | Per-user licence assignments (friendly names), enabled state and last interactive sign-in for every member user | | get_device_compliance | Intune devices by OS/version, compliance state counts, 30-day-stale list, per-device rows | | get_teams_channels | Teams and channels inventory: channel types (standard/private/shared), members with roles, external participants flagged, shared-with teams | | get_tenant_security_summary | The five security sections composed, failing independently (complete + failed_sections flags) | | get_admin_audit | Privileged roles: who holds them, Global Admin count, admins without MFA, disabled accounts with roles | | get_oauth_app_audit | Third-party enterprise apps and OAuth grants with permission names and risk flags | | get_stale_accounts | Dormant users/guests (needs Entra P1 for sign-in data) and licences on disabled/dormant accounts | | get_ca_baseline_gaps | Conditional access pass/fail/warn against the Superfast baseline, with evidence | | get_dns_email_security | SPF/DKIM/DMARC per verified domain (public DNS lookups) | | get_mailbox_usage | Mailbox/OneDrive/SharePoint storage and activity, near-quota and inactive mailboxes | | get_defender_incidents | Defender XDR incidents (90 days): severity/status counts, open incident list | | get_intune_apps | Apps published through Intune: type, platform, publisher, version, publishing state, deployment assignments; flags published-but-unassigned apps | | get_intune_baseline | Compliance policies and config profiles, assignment gaps, BitLocker enforcement | | get_forwarding_rules | User inbox rules that forward externally, BEC forward-and-delete patterns | | get_sharing_audit | Tenant sharing policy (anonymous links etc.), site inventory with dormant sites, groups/team sites with guests | | get_estate_summary | Cross-tenant league table: secure score, MFA coverage, device health for every customer |

Every result includes tenant_id, display_name and retrieved_at (ISO 8601).

Adding to Claude Desktop / Cowork

claude_desktop_config.json (Claude Desktop → Settings → Developer → Edit Config), or the equivalent MCP server entry in Cowork:

{
  "mcpServers": {
    "ms365-gdap": {
      "command": "node",
      "args": ["/Users/jamescash/mcp/m365-gdap/dist/index.js"],
      "env": {
        "PARTNER_TENANT_ID": "<partner tenant GUID>",
        "APP_CLIENT_ID": "<app client GUID>",
        "APP_CLIENT_SECRET": "<secret>",
        "TENANTS_FILE": "/Users/jamescash/mcp/m365-gdap/tenants.json"
      }
    }
  }
}

For Claude Code: claude mcp add ms365-gdap --env PARTNER_TENANT_ID=… --env APP_CLIENT_ID=… --env APP_CLIENT_SECRET=… --env TENANTS_FILE=… -- node /Users/jamescash/mcp/m365-gdap/dist/index.js

Troubleshooting the three most likely auth failures

Every error the server returns carries a kind, a message and a remediation. The three you will actually see:

1. not_consented — "app was not found in the directory" (AADSTS700016). The app has no service principal in that customer tenant. Run the consent step (section 3) for that customer, wait a couple of minutes for replication, retry. This is what every tenant looks like before onboarding, and what a tenant looks like if a customer admin deleted the enterprise application.

2. missing_permission — Graph returns 403 Authorization_RequestDenied. The token was issued but lacks the app role for that endpoint. Usually the permission was added to the app registration after the tenant was consented — consent is per-tenant and does not update itself. Delete and re-create the consent for that customer (section 3), or re-run the admin-consent URL. Check the permission table above against the app registration's API permissions blade.

3. credential_error — invalid client secret (AADSTS7000215). The secret is wrong or (more likely, eighteen months from now) expired. Create a new secret on the app registration, update APP_CLIENT_SECRET everywhere it is configured (Claude Desktop config, .env), restart the MCP server.

Honourable mention: intune_unavailable on get_device_compliance is not an auth failure — the tenant has no Intune licence or Intune was never provisioned. The summary tool records this in failed_sections and carries on; report it as "not licensed for Intune" in the SBR.

Read-only guarantees

  • The Graph client exposes only GET; no code path constructs another verb.
  • enforceReadOnlyFetch() patches global fetch at startup: any non-GET request to a Microsoft Graph host throws read_only_violation before the request is sent (token POSTs to login.microsoftonline.com are authentication, not writes, and are allowed).
  • The app registration holds no write application permissions, so even a bug could not mutate a tenant — Graph would refuse it.

Development

src/
  index.ts          MCP server + tool registration (stdio)
  auth.ts           per-tenant client-credential tokens with expiry cache
  graph.ts          GET-only Graph client: retry/backoff (Retry-After aware), pagination
  tenants.ts        tenants.json loading + name/GUID/partial resolution
  skus.ts           SKU GUID → friendly product name mapping
  errors.ts         typed error classification (consent vs permission vs Intune…)
  readonly.ts       global non-GET fetch guard
  tools/            one module per MCP tool
  cli/check.ts      npm run check -- "<tenant>" live smoke test
test/               vitest: SKU mapping, tenant resolution, pagination/retry, mock-Graph integration