Blog

Claude and Graph Without a Tenant-Wide Token

·7 min read

You can connect Claude or Copilot to Microsoft Graph without giving the agent a tenant-wide token. Graph application permissions such as Mail.Read usually apply to every mailbox in the tenant. The tenant-wide token stays behind the proxy. The agent receives a scoped caller credential and only the mailboxes, folders and operations you grant.

The public repository geoicons/graphapi-mcp is a thin Model Context Protocol (MCP) client for this pattern. Console screenshots in this guide are placeholders. Do not treat placeholder captions as UI labels.

The helpdesk bot that could read the CEO

A Microsoft 365 administrator at a mid-market firm created an app registration so an assistant could “read the support shared mailbox.” The quickest Graph permission that worked was application Mail.Read. That permission is not “this mailbox.” It is the tenant. The assistant, and anyone who later copied its config, could request /users/{any-id}/messages. The admin had intended a queue. They had authorised a census.

Delegated permissions would have bound the app to a signed-in user. Application permissions bound it to the tenant. Neither choice, by itself, stopped the agent holding the token.

Delegated versus application permissions

Delegated permissions act as a user. Application permissions act as the app, often across the tenant.

DelegatedApplication
Who is the principalSigned-in user plus the appThe app (service principal)
Typical mail readMail.Read on that user’s mailbox (and shared mailboxes they can access)Mail.Read on all mailboxes unless you add extra restrictions in Exchange
Fits an unattended agentPoorly – someone must sign inWell – and that is the risk
Blast radius if the token leaksThat user’s reachOften the tenant

Microsoft documents application Mail.Read as read-all-mailboxes. You can reduce that in Exchange Online with application access policies or the newer RBAC-for-applications model. Do that. Still do not put the resulting token in Claude.

A leaked application token is a tenant incident. A leaked proxy credential is a grant incident: only the paths you allowed, only until you revoke the caller.

Microsoft’s permissions reference is explicit that administrators can configure an application access policy so an app with application Mail.Read does not in fact reach every mailbox. Use that control. It still leaves a Microsoft-issued credential that works at Graph for the mailboxes you included. The proxy is the second fence: the agent never holds that credential, and a path outside the grant never leaves your environment.

Application access policy and RBAC-for-applications are Microsoft-side fences. They are necessary and they are not enough for an agent that will follow tool instructions. A prompt that says “list users, then read the CEO” will still attempt /users and /users/{id}/messages. Graph will answer if the token allows it. A proxy grant that lists only GET /users/{support-mailbox}/mailFolders/{id}/messages turns that attempt into a deny row you can show the security owner. The token never left the proxy to try.

Why tenant-wide Mail.Read is the wrong default for an agent

Because the agent’s task is almost never “every mailbox.”

A helpdesk assistant needs one shared mailbox, or one folder. A calendar assistant needs Calendars.Read for named rooms, not Calendars.ReadWrite on the firm. Graph will happily return more than the task if the token says so. The model will put it in context.

The same isolation as Claude to Xero applies: narrow consent at the vendor where you can, and a deny-by-default proxy in front. Credential-less agents is the pattern. Outbound API governance is the category.

How to use graphapi-mcp with a RequestRocket proxy

The repository is a small Python MCP server with one tool, graphapi. It does not talk to Graph with a Microsoft token. It talks to a RequestRocket proxy URL with a RequestRocket key.

Steps below follow the README.

1. Create the Graph app and prefer the narrowest permission

In Microsoft Entra, register an app. If the agent is unattended, you will use application permissions. Prefer resource-specific consent or Exchange application access policies so Mail.Read is not in fact every mailbox. Grant admin consent.

[SCREENSHOT: Entra app registration – application Mail.Read plus mailbox restriction]

2. Store the Graph credential on a RequestRocket target

Create a target for https://graph.microsoft.com/v1.0. Attach a target credential that holds the app’s client credentials (or the managed secret your admin process already uses). Claude never sees those values.

[SCREENSHOT: target – Graph base URL, OAuth2 or client-credential type]

3. Create a deny-by-default proxy and a caller credential

Create a proxy on that target. Issue a proxy credential for this assistant only. Default rule effect: deny.

[SCREENSHOT: proxy – default deny]

4. Write scope rules

Allow only the methods and paths the task needs. Examples of grants to consider, then tighten:

  • Helpdesk: GET on /users/{support-mailbox-id}/mailFolders/{folder}/messages and message-by-id reads.
  • Calendar: GET on /users/{room}/calendarView only.
  • Deny POST /sendMail, deny /users/ list, deny other users’ mailboxes.

[SCREENSHOT: rules – named mailbox GET allowed, sendMail denied]

5. Configure graphapi-mcp

From the README:

pip install -r requirements.txt

Copy .env.example to config.env or .env next to server.py and set:

  • GRAPH_API_REQUESTROCKET_URL – the proxy base URL (trailing slash optional; the server strips it)
  • GRAPH_API_REQUESTROCKET_KEY – value sent as the Authorization header

Alternatively export those variables, or point DOTENV_PATH at another file. Run:

python server.py

The server uses stdio (FastMCP). On start it logs the proxy URL and the tool signature.

The graphapi tool accepts method (GET, POST, PATCH, PUT, DELETE), path (for example /me/messages; a leading https://graph.microsoft.com/v1.0 prefix is stripped), optional params, and optional body.

[SCREENSHOT: Claude or Cowork local MCP connector – command is python /path/to/graph-mcp/server.py]

The README’s Cowork example is:

python /path/to/graph-mcp/server.py

If credentials live elsewhere:

DOTENV_PATH=/Users/you/project/config.env

6. Test

Succeed:

Show the five most recent messages in the support inbox. Subject and from only.

Fail:

List every user in the tenant, then read the CEO’s mail.

The first should return a short list. The second should be denied at the proxy. Graph should not see it.

[SCREENSHOT: chat – allowed inbox GET, denied /users list]

7. Read the audit trail

The proxy log should show the allowed GET and the denied path, both under this proxy credential.

[SCREENSHOT: request log – GET allowed, list users denied]

What to check

  1. The Entra permission is as narrow as Exchange or RSC will allow.
  2. The Graph client secret is only on the target.
  3. GRAPH_API_REQUESTROCKET_KEY is a proxy credential, not a Microsoft token.
  4. Default deny is on. Send-mail is not in grant unless you intended it.
  5. A prompt for another user’s mailbox is denied in the log.

Where RequestRocket fits

RequestRocket holds the Graph token, applies the mailbox and method rules, can filter message bodies, meters the assistant, and records the trail. graphapi-mcp is a thin MCP adapter. RequestRocket does not replace Entra ID, and it does not provide Claude or Copilot.

FAQ

Can we use delegated Mail.Read instead?

Yes, if a user signs in and the agent’s reach should be that user’s. Unattended agents usually cannot. Application plus a proxy is the unattended pattern.

Does this work with Microsoft Copilot?

The proxy pattern does. The MCP connector in this repo is documented for a local Python stdio server (Claude / Cowork). Copilot’s connector UI is a different screen – capture it before treating labels as final.

What if we need to send mail?

Add an explicit POST rule on the send path for that mailbox only. Keep it off the default helpdesk proxy.

Is /me/ safe with application permissions?

/me/ is a delegated convention. Application calls should use /users/{id}/.... Put that in the allow rule so a guessed /users/{ceo}/messages still misses.

Do we have to use this GitHub repo?

No. Any client that can send HTTP to the proxy with the proxy credential works. This repo is the documented RequestRocket-enabled Graph MCP server.

Why not put the Graph token in the MCP env file?

Because that file is the leak. The README’s env vars are the proxy URL and proxy key only.

Book a walkthrough of RequestRocket

Sources

Enhance ISO 27001
Enhance SOC 2
Enhance GDPR
Enhance HIPAA

Add outbound API security
without changing code

Start on your own or talk to our team about improving the security of every API call you make.