MSP all tenants in one question

21 Oct 2026

Which client has the most licence waste? One question for an MSP

Seven clients, seven admin centres, one question. The Microsoft 365 usage report that answers "who hasn't used their licence" is a per-tenant CSV: one download per client, one spreadsheet to stitch them, and by the time the last tab is open the first one is a day old. GDAP gets a partner into each client's admin portals, partitioned per customer. The comparison between clients is still the spreadsheet.

This is the playbook for doing it as one question with the GraphPaaS connector for Claude. Every call below was made on our demo organisation, which holds two synthetic tenants, Fabrikam HQ and Fabrikam Iberia. Two clients instead of seven, but the calls don't change with the count, and neither does the answer's shape: one row per client.

WHAT tenant_id="all" ACTUALLY READS Your organisation every client tenant Your scope user or key tenant list Not paused for read-only members No demo tenants once a real one exists Client A rows tenant = "Client A" Client B rows tenant = "Client B" No such domain? listed under unavailable group_by="tenant" one count per client, one call

Six steps from "which client" to a table

1
Ask without naming a client.

The connector won't guess. With more than one tenant readable and no tenant_id, it refuses and hands Claude the list, so Claude asks you to pick: one option per client, plus "All tenants". A silent default would answer about a client you never meant. Done when the options Claude offers match your client list.

You ask

Which client has the most licence waste?

Claude calls
get_metrics(domain="licence_waste", group_by="tenant")
→ 400: you can read 2 tenants — ask the user which one they mean
  (one option per tenant plus 'All tenants' (tenant_id="all"))
Claude answers

You can read two tenants: Fabrikam HQ (Demo) and Fabrikam Iberia (Demo). Which one, or all tenants?

2
Count the licensed users per client first.

The licence_waste domain holds every user with a paid licence, not only the wasted ones, so a group_by with no filter is the denominator. In the demo: 69 rows, 42 in HQ and 27 in Iberia. Each tenant also carries its own collected_at (HQ on 18 October at 05:45 UTC, Iberia on 17 October at 17:57 UTC), because Microsoft publishes this report once a day per tenant. Done when you have one count per client and know how old each one is.

3
Count the waste in two calls, not one.

A licence is wasted when its last activity is older than 90 days or missing altogether: Microsoft drops the date for people who were never active, or not for a long time. where ANDs its conditions, so "null or stale" is two questions. Done when both counts are in, per client.

You ask

All tenants. Which client has the most licence waste?

Claude calls
get_metrics(domain="licence_waste", tenant_id="all", group_by="tenant")
→ Fabrikam HQ (Demo) 42 · Fabrikam Iberia (Demo) 27
get_metrics(domain="licence_waste", tenant_id="all",
            where={"last_activity": null}, group_by="tenant")
→ Fabrikam HQ (Demo) 5 · Fabrikam Iberia (Demo) 3
get_metrics(domain="licence_waste", tenant_id="all",
            where={"last_activity": {"<": "2026-07-20"}}, group_by="tenant")
→ Fabrikam HQ (Demo) 5 · Fabrikam Iberia (Demo) 3
Claude answers

Fabrikam HQ: 10 of its 42 licensed users (24%) show no activity in 90 days, five with no date at all. Fabrikam Iberia: 6 of 27 (22%). HQ leads on both the count and the rate, but only just on the rate.

4
Rank by rate, act on the count.

A count always favours your biggest client. The rate says who manages licences worse; the count says where a renewal call saves the most. Here they agree. Across seven clients of different sizes they usually won't, and the list of names behind each count is one more call with columns. Our licence review in one prompt walks that per-client follow-up. Done when each client has a rate and a named list.

5
Ask the same of MFA, then of the admins.

The mfa_registration domain comes from Microsoft's userRegistrationDetails, where isMfaRegistered means a strong method is registered, even one the authentication methods policy doesn't allow, and isAdmin flags anyone in an admin role. The overall rate is the slide for the client; the admins are the ticket. Done when every admin without MFA has a name next to a client.

You ask

And MFA coverage per client? Any admins without it?

Claude calls
get_metrics(domain="mfa_registration", tenant_id="all",
            where={"mfa_registered": false}, group_by="tenant")
→ Fabrikam HQ (Demo) 7 · Fabrikam Iberia (Demo) 5
get_metrics(domain="mfa_registration", tenant_id="all",
            where={"mfa_registered": false, "is_admin": true},
            columns=["tenant", "display_name", "user_type", "methods"])
→ 4 rows
Claude answers

HQ has 35 of 42 users registered (83%), Iberia 22 of 27 (81%). Each has two admins with no method registered at all: Ana Dias and Diego Yilmaz, members in both tenants.

Same names in both tenants is the demo showing its seams: it reuses the same synthetic people in both tenants. In a real portfolio, the same name across two clients is worth a second look of its own: a contractor, or one of your own technicians with a local account in each.

Client (demo) Licensed users Idle 90 days Waste rate MFA registered Admins without MFA
Fabrikam HQ 42 10 (5 with no date) 24% 35 of 42 (83%) 2
Fabrikam Iberia 27 6 (3 with no date) 22% 22 of 27 (81%) 2

Five calls, one table. With seven clients it is still five calls; only the table grows.

6
Give each technician only their clients.

"All" never means the whole organisation. It means the tenants the caller can read. A member restricted to three clients gets those three in the dashboard's all-tenants views and in Claude, and nothing else; an API key limited to them gets the same three. It's the per-customer partition GDAP brought to partner access, applied to the reading. Done when a restricted technician's list_tenants shows only their clients.

Why the demo tenants stay out of your sums

Every organisation can add demo tenants, to show a client what a report looks like without opening a real one. They are synthetic, and a sum that adds them to real clients is a wrong number with a straight face. So the rule is in the connector: once an organisation has one real tenant, tenant_id="all" leaves the demo tenants out. Name one explicitly and it still answers. Our demo organisation has no real tenant, which is the only reason both Fabrikam tenants show up in the calls above.

Tenants that can't answer don't vanish either. A client without the licence or the permission for a domain is listed under unavailable with the reason, so Claude says which client can't answer instead of ranking it as the cleanest one. The same holds for MFA gaps: our MFA registration gaps article covers what the registration report doesn't see.

The pattern
tenant_id="all" plus group_by="tenant" turns any per-tenant question into a per-client ranking: waste, MFA, guests, stale devices. Ask for the rate, not only the count, check each tenant's collected_at, and read the unavailable list before you call any client clean.
Sources
GraphPaaS

Ask every client tenant the same question, and get one row per client back.

Add the connector →

Newsletter

Get the next article by email

Practical M365 & Azure cost pieces like this one. No spam, unsubscribe anytime.