Skip to content

MCP and OAuth ​

MCP clients such as Claude Code, Cursor and Claude Desktop reach your actions through one server the package mounts, as the person whose token they hold.

  • Claude Codeclaude mcp add
  • Cursor.cursor/mcp.json
  • Claude Desktopthrough mcp-remote
the package's MCP serverPOST mcp/actionsPOST mcp/t/{tenant}
  1. auth:sanctumreads the bearer token
    no valid token: 401
  2. throttle60 a minute, per person
    past it: 429
  3. the MCP doorRead or Write, and #[Expose] allows MCP
    never listed
  4. token abilitiesnamed by the token; * never counts
    not named: not listed
  5. the rest of the pipelinemembership, authorize(), validation, handle()
Every request passes the guard and the throttle, then the same pipeline an agent's call takes.

One server, one tool per action ​

The server answers at mcp/actions. When your actions are tenant-scoped, set mcp.tenant_path (such as mcp/t/{team}, with your tenant.parameter in the braces) and they are served there instead, one URL per tenant. Each Read or Write action whose #[Expose] allows MCP is one tool. See Where it is mounted.

A token names what it reaches ​

A client connects with a person's token, and over MCP an ability counts only when the token names it: actions:read, actions:write, and tenant:{key} to bind it to one tenant. Your app mints it; the package ships no token page.

php artisan tinker
php
$user->createToken('Claude Code', [
    'actions:read',
    'actions:write',
    'tenant:'.$team->getKey(),
], now()->addDays(90));
The token namesRead actionsWrite actionsDestructive, External
actions:readyesnono
actions:read, actions:writeyesyesno
*nonono
Sanctum's default token, ['*'], lists no tools over MCP.

The client then runs as that person, and never does more than the person could in the app: membership, authorize() and validation run on every call. See Tokens and abilities.

One budget per person ​

All of one person's tokens and tenant URLs share one budget, mcp.per_minute. See Throttle.

Remote clients sign in with OAuth ​

A Claude custom connector or ChatGPT connects from its provider's cloud and has no field for a pasted token. Add Laravel Passport and name its guard in mcp.middleware: that one line is the switch.

config/agentic-actions.php
php
'mcp' => [
    // ...
    'middleware' => ['auth:sanctum,api', 'throttle:agentic-actions-mcp'],
],
  1. The person adds the URLa custom connector, with mcp/t/design
  2. 401 names the scopesand the path's OAuth metadata
  3. The client registersthen the person signs in
  4. Consent screenthe person approves one URL
  5. One connectionits token reaches only that URL
  6. revoke()the next call answers 401
The person approves one URL; the client's token reaches only that URL, within the abilities they allowed.

The consent screen appears for every authorization that names one of the package's MCP URLs, even for a client approved before. See OAuth.

Connections can be revoked ​

Each approval records one connection per client and person. List them on the person's settings page, and revoke() ends the client's tokens at once: its next call answers 401. See Connected apps, and the guarantees in Security.

Released under the MIT License.