# auth.md

**Authenticating an agent with Moonage.** Moonage is the organizational system
for humans and agents. This document is the procedural recipe for getting an access token
so your agent can call the Moonage MCP server on a human's behalf.

Everything here is standard OAuth 2.1. If your client already speaks MCP
authorization, you do not need this document — point it at
`https://mcp.moonage.ai/mcp` and let the 401 challenge drive discovery.

| | |
|---|---|
| Protected resource | `https://mcp.moonage.ai/mcp` |
| Transport | Streamable HTTP |
| Authorization server | `https://auth.moonage.ai` |
| Client authentication | None — public clients, PKCE required |
| Registration | RFC 7591 dynamic client registration |

---

## Step 1 — Discover

Discovery is two hops. Never hardcode the endpoints below; read them from
the metadata so a future migration does not strand your client.

### 1a. Fetch the Protected Resource Metadata

An unauthenticated request to the MCP endpoint returns `401` with a
`WWW-Authenticate` header naming its metadata document (RFC 9728):

```http
POST https://mcp.moonage.ai/mcp

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.moonage.ai/.well-known/oauth-protected-resource/mcp"
```

```http
GET https://mcp.moonage.ai/.well-known/oauth-protected-resource/mcp
```

```json
{
  "resource": "https://mcp.moonage.ai/mcp",
  "authorization_servers": ["https://auth.moonage.ai"],
  "scopes_supported": ["moonage:ask", "agents:message", "memory:read", "memory:write", "task:read", "task:write", "integrations:read", "integrations:act", "governance:admin"],
  "bearer_methods_supported": ["header"]
}
```

`resource` is the value you MUST send as the `resource` parameter in
Step 4 and Step 5 (RFC 8707 audience binding).

### 1b. Fetch the Authorization Server metadata

Take `authorization_servers[0]` and fetch its RFC 8414 document:

```http
GET https://auth.moonage.ai/.well-known/oauth-authorization-server
```

```json
{
  "issuer": "https://auth.moonage.ai",
  "authorization_endpoint": "https://auth.moonage.ai/oauth/authorize",
  "token_endpoint": "https://auth.moonage.ai/oauth/token",
  "registration_endpoint": "https://auth.moonage.ai/oauth/register",
  "revocation_endpoint": "https://auth.moonage.ai/oauth/revoke",
  "jwks_uri": "https://auth.moonage.ai/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "client_id_metadata_document_supported": true
}
```

Verify that `issuer` equals the origin you fetched it from. It will not
match `https://moonage.ai` — the apex only redirects here, it is not an
issuer.

---

## Step 2 — Pick a method

Two ways to become a client. Both produce a public client: there is no
client secret, and `token_endpoint_auth_method` is `none`, so PKCE is what
protects the authorization code.

- **Dynamic client registration (RFC 7591)** — recommended, and what every
  current MCP client uses. One POST, no human in the loop. Go to Step 3.
- **Client ID Metadata Document** — the AS advertises
  `client_id_metadata_document_supported: true`, so you may instead use an
  `https` URL that resolves to your client metadata as the `client_id`
  and skip registration entirely. Useful if you cannot persist a
  registration. Skip to Step 4.

---

## Step 3 — Register

```http
POST https://auth.moonage.ai/oauth/register
Content-Type: application/json

{
  "client_name": "Your Agent",
  "redirect_uris": ["http://127.0.0.1/callback"]
}
```

```http
HTTP/1.1 201 Created
Cache-Control: no-store
```

```json
{
  "client_id": "…",
  "client_name": "Your Agent",
  "redirect_uris": ["http://127.0.0.1/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1750000000
}
```

`redirect_uris` is required: 1–10 entries, each at most 2048 characters.
`client_name` is optional (max 256 characters). No client secret is
issued. Persist the `client_id`.

### Redirect URIs

Redirect URIs are matched **exactly** (RFC 6749 §3.1.2.3), with one
relaxation — RFC 8252 §7.3 loopback interface redirection:

> A native app that receives an OS-assigned ephemeral port may register a
> portless `http://localhost/callback` or `http://127.0.0.1/callback` and
> then redirect to the same URI on **any** port.

The relaxation is loopback-HTTP only. Scheme, host, path and query must
match; only the port may differ. It never applies to `https`, never to a
non-loopback host, and a URI carrying a fragment is rejected. If you are a
native or CLI client, register the portless loopback form — do not try to
register every port you might bind.

---

## Step 4 — Authorize

Send the person to the authorization endpoint. PKCE is mandatory and
`S256` is the only supported challenge method.

```
GET https://auth.moonage.ai/oauth/authorize
  ?response_type=code
  &client_id=…
  &redirect_uri=http://127.0.0.1:51234/callback
  &code_challenge=…
  &code_challenge_method=S256
  &state=…
  &resource=https://mcp.moonage.ai/mcp
  &scope=moonage:ask task:read task:write
```

Request the narrowest set of scopes that does the job. Moonage shows the
person a consent screen itemizing exactly what you asked for; a long list
costs you approvals.

On approval the AS redirects to your `redirect_uri` with `code` and your
`state`. Verify `state` before continuing.

---

## Step 5 — Exchange the code

```http
POST https://auth.moonage.ai/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=…
&redirect_uri=http://127.0.0.1:51234/callback
&client_id=…
&code_verifier=…
&resource=https://mcp.moonage.ai/mcp
```

You receive an `access_token`, a `token_type` of `Bearer`, an
`expires_in`, and a `refresh_token`. Refresh with:

```http
POST https://auth.moonage.ai/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=…&client_id=…&resource=https://mcp.moonage.ai/mcp
```

---

## Step 6 — Use the access_token

Present the token in the `Authorization` header — `bearer_methods_supported`
is `["header"]`, so a query parameter will not work.

```http
POST https://mcp.moonage.ai/mcp
Authorization: Bearer <access_token>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
```

**Call `moonage_context` first.** It returns who the person is, their
organization, the agents they can reach, the spaces you can put work in,
and the scopes you were actually granted. Every other tool takes ids that
come from it. Moonage ids are opaque and are never guessable — copy them
from a tool result, never construct one. If `spaces` comes back empty,
say so and stop; no id you invent will work.

Moonage reports no default timezone. Name an IANA zone yourself
(e.g. `Europe/Istanbul`) in anything you schedule.

---

## Scopes

| Scope | What it unlocks |
|---|---|
| `moonage:ask` | Ask `@moonage`; list the organization's agents |
| `agents:message` | Send a named agent something to act on now |
| `memory:read` | Search and read company memory, with provenance |
| `memory:write` | Record a fact, decision, or standing instruction |
| `task:read` | Read tracked work and its progress |
| `task:write` | Create, answer, and cancel tracked work |
| `integrations:read` | Discover and describe connected capabilities |
| `integrations:act` | Execute a consequential capability |
| `governance:admin` | List and resolve pending decisions |

### What a scope does not do

A scope is a **ceiling, not a grant**. It narrows what your token may ask
for; it never authorizes an action by itself. The live decision is computed
at dispatch by the Moonage runtime gate, against the acting agent's
autonomy level, the capability's risk tier, the organization's standing
rules, the remaining budget, and the responsible human.

So: holding `integrations:act` does not mean an action executes. Anything
consequential pauses and surfaces to a human in the Moonage Inbox. Your
client should treat a paused result as a normal, expected outcome — not an
error, and not something to retry around. Tools that start work return an
op id immediately and complete asynchronously; poll rather than block.

---

## Errors

| Status | Error | What to do |
|---|---|---|
| 401 | — | Token missing, expired, or revoked. Refresh; if that fails, restart at Step 1. The `WWW-Authenticate` header carries the resource metadata URL. |
| 403 | `insufficient_scope` | The response names the `required_scope`. You cannot widen a token — send the person back through Step 4 with the added scope. |
| 400 | `invalid_redirect_uri` | The `redirect_uri` does not match what you registered. Check the loopback rules in Step 3. |
| 400 | `invalid_grant` | The code was already used, expired, or the `code_verifier` does not match the challenge. Restart at Step 4. |
| 413 | — | Request body over 1 MiB, or a JSON-RPC batch longer than 25. Split it. |
| 500 | — | A tool exceeded its 25-second budget. The underlying work may still complete server-side — retry with the same `idempotency_key` rather than issuing a fresh call. |

Write tools accept an `idempotency_key`. Use one. It is the difference
between a retried timeout reusing the original operation and creating a
duplicate.

---

## Revocation

Revoke either token at the RFC 7009 endpoint:

```http
POST https://auth.moonage.ai/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=…&client_id=…
```

Revocation is also a human action: a person can withdraw your client's
access from Moonage at any time, without telling you first. Treat a sudden
`401` that a refresh cannot fix as a deliberate revocation — stop, discard
the stored tokens, and ask the person to reconnect. Do not silently
re-register to get back in.

---

## More

- MCP Server Card — `https://moonage.ai/.well-known/mcp/server-card.json`
- Agent Skills index — `https://moonage.ai/.well-known/agent-skills/index.json`
- Documentation — `https://moonage.ai/docs`
- Security — `https://moonage.ai/security`
