# auth.md

Zerply exposes an MCP server that reads and writes data inside a user's Zerply
workspace. This file tells an agent how to get a token for it.

## Audience

This applies to autonomous agents and MCP clients that act for a signed-up
Zerply user. There is no public or anonymous tier. Every call is scoped to a
workspace (a "team") that the user already belongs to.

## Resource

| Field | Value |
|-------|-------|
| Protected resource | `https://api.zerply.ai/` |
| MCP endpoint | `https://api.zerply.ai/mcp` |
| Transport | Streamable HTTP |
| Resource metadata | `https://api.zerply.ai/.well-known/oauth-protected-resource` |

An unauthenticated request to the MCP endpoint returns `401` with a
`WWW-Authenticate` header that points back at the resource metadata, as
described in RFC 9728.

## Authorization server

| Field | Value |
|-------|-------|
| Issuer | `https://api.zerply.ai` |
| Metadata | `https://api.zerply.ai/.well-known/oauth-authorization-server` |
| Registration | `https://api.zerply.ai/oauth/register` |
| Authorization | `https://api.zerply.ai/oauth/authorize` |
| Token | `https://api.zerply.ai/oauth/token` |

Read the metadata document before you build a request. The values above are a
convenience copy and the metadata wins if the two ever disagree.

## Register

Zerply supports OAuth 2.0 Dynamic Client Registration (RFC 7591). Post your
client metadata to the registration endpoint and keep the `client_id` you get
back. No pre-shared secret is needed and none is issued.

```
POST https://api.zerply.ai/oauth/register
Content-Type: application/json

{
  "client_name": "Your agent",
  "redirect_uris": ["https://your-agent.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp"
}
```

## Get a token

Zerply issues tokens through the authorization code flow with PKCE. Clients are
public, so `token_endpoint_auth_method` is `none` and the code verifier is what
proves the client. Only the `S256` challenge method is accepted.

1. Generate a code verifier and its `S256` challenge.
2. Send the user to `/oauth/authorize` with `response_type=code`,
   your `client_id`, `redirect_uri`, `code_challenge`,
   `code_challenge_method=S256`, `state`, and `scope=mcp`.
3. The user signs in and approves the connection in a browser. This step needs a
   human the first time.
4. Exchange the returned code at `/oauth/token` with `grant_type=authorization_code`
   and your code verifier.
5. Store the refresh token and use `grant_type=refresh_token` after that.

## Scope

There is one scope, `mcp`, and every token carries it. It is deliberately
coarse: it grants the agent the same workspace access the approving user has.
Per-tool and read-only scopes do not exist yet, so treat any token as able to
read and write content in that user's workspaces. The token response echoes
`"scope": "mcp"` so you can confirm what you were granted.

## Use the token

Send it as a bearer token in the `Authorization` header:

```
Authorization: Bearer <access_token>
```

Tokens carry the user's workspace access. Call `get_my_teams` first, then pass an
explicit `team_id` on every team-scoped tool call. Do not guess a `team_id`.

## Revoke

Users manage and revoke agent connections at `https://app.zerply.ai/mcp`. A
revoked connection makes both the access token and the refresh token fail, and
the agent has to run the authorization flow again.

## More

- MCP setup guide: `https://zerply.ai/docs/mcp/`
- Agent skill: `https://zerply.ai/.well-known/agent-skills/zerply/SKILL.md`
- Server card: `https://zerply.ai/.well-known/mcp/server-card.json`
- API catalog: `https://zerply.ai/.well-known/api-catalog`
