Module

MCP

An MCP endpoint of your app's own, at /mcp on your Worker. Your users point an AI agent at it, approve it on your own origin, and it calls your routes as them: the tools their role reaches, and nothing else, each call in your audit trail.

@pithy-sh/auth

terminal
pithy add mcp --floor power:agents:use

What it is for

Letting your users bring their own agent. Someone pastes your app's /mcp address into Claude, ChatGPT or another MCP client. The client finds your sign-in on its own, the person signs in on your origin and approves it, and the agent receives a token bound to that one endpoint, so it cannot be replayed anywhere else.

Your routes are the tools. A route opts in where it is declared: tool: true on a read, or the effect on anything else, which is one of writes, sends, charges or deletes. Its name is <capability>_<operation>, permanent once published. Each caller is offered only the tools their role reaches, by the same check the route makes, so a role that gains a power is an agent that sees more with no deploy.

The agent is the user, and nothing more. A tool call runs as a request to the route through your whole app, so every guard and every power check runs exactly as it does for the person's own client. It never gets a power the person does not hold.

Screens you would otherwise write. pithy ui add scaffolds the consent screen and the connected-applications screen beside sign-in. The consent screen is shown every time a client asks, lists what the agent will be able to do per capability, and shows the name the client gave itself as unchecked, with the emphasis on what is checked: where the access will be sent and, for a client known by its metadata document, the host that serves it. The connected-applications screen shows that host too. It lists what a person approved and revokes it, with no administrator involved.

A record of what agents did. Every tool call is an audit row naming the user, the session they approved it in and the client, marked as an agent, with its outcome, status, duration and response size. It keeps the shape of the arguments and never their values.

What it sits on

Your Worker, your D1, and auth's own instance.

Composing it adds an OAuth 2.1 authorization server to the auth instance you already run, built on Better Auth's OAuth provider: PKCE, authorization codes and refresh tokens, and access tokens signed by the keys auth already publishes. Its tables live beside auth's in the same D1.

Dev, staging and production each publish their own issuer and endpoint, from the same address auth already resolves for that environment, so nothing needs overriding per environment.

  • D1
  • Workers

The honest bit

Off until you compose it, and closed when you do.

An endpoint that lets any signed-in user drive an agent at your app is remote automation of screens designed for a person at a keyboard, and that changes rate, cost and abuse even when every gate holds. So there is no endpoint until you compose mcp({ floor }), and the floor has no default: every signed-in user, a power, or an entitlement. pithy doctor prints which one is in force, in words when it is everyone.

A route is a tool only when it says so. Nothing is offered by accident, and a route that sends mail, charges a card or deletes something has to say which before it can be one.

Consent is one decision today: let this client act as you in this app. The screen shows per capability what that means, but there is nothing to untick, so revoking is how a person narrows it. It needs auth beside it, and refuses a second OAuth provider in auth({ plugins }).

If you build it yourself

An MCP server is an afternoon. An authorization server is not.

The tools are the easy part. What makes it safe to hand to your users is everything around them.

An OAuth 2.1 server

Authorization codes with PKCE, refresh tokens, discovery documents a client can find from a bare URL, clients known by the metadata document they publish or registered dynamically, and redirect addresses enforced, inside the same Worker that runs your sign-in.

Tokens bound to one endpoint

A token minted for production must not open staging, and one minted for your endpoint must not open anybody else's. Each environment publishes its own issuer and resource, from its own address.

A consent screen that is not a phishing page

Any client picks its own name. A screen that prints it as a fact invites the first impostor in, so the name is shown as reported and the emphasis goes to what can be checked: where the access will be sent, and the host that serves a client's metadata document.

A tool list filtered per caller

A tool a role withholds has to be absent, not listed and refused, and it has to be filtered by the same check the route makes, or the two drift apart.

An audit row that copies nothing

An agent's arguments carry your customers' data. A trail that copied them would be a second place that data lives, so it keeps their shape and never their values.

A revoke the user can reach

With nothing to untick on the consent screen, revoking is how a person says no. It has to be on their own screen, without an administrator, and the next call has to fail.

It is composed from Better Auth's OAuth provider, so the protocol is the one the ecosystem already reviewed, and what Pithy adds is the tool list, the screens and the record.

terminal
pithy add mcp --floor power:agents:use

In the dashboard

Your users manage their own connections.

Each person sees and revokes the agents they approved on the connected-applications screen in your app. The dashboard has no pane for those connections, and every tool call is a row in the audit trail it already reads.

Add it, and get on with the feature.

One command installs the package, wires its bindings into every environment your project declares, and runs its migrations.