# Agent authentication on halulu.food

Halulu is an AI restaurant finder. Its one programmatic surface is an MCP server at
`https://www.halulu.food/api/mcp`, protected by OAuth 2.1. Contact: eat@halulu.food.
Last updated 2026-08-22.

## Discover

Call the API with no credential and read the challenge (GET or POST, no auth):

```http
POST https://www.halulu.food/api/mcp
```

It answers `401` with:

```
WWW-Authenticate: Bearer resource_metadata="https://www.halulu.food/.well-known/oauth-protected-resource", scope="openid profile email"
```

Then fetch, in order:

1. `https://www.halulu.food/.well-known/oauth-protected-resource` (RFC 9728).
2. `https://www.halulu.food/.well-known/oauth-authorization-server/api/mcp-auth` (RFC 8414). The issuer carries a path, so the well-known segment is INSERTED before it.
3. `https://www.halulu.food/api/mcp-auth/jwks` for the signing keys.

Index of every API document: `https://www.halulu.food/.well-known/api-catalog` (RFC 9727).

## Pick a method

There is exactly one way to reach a user's data: OAuth 2.1 authorization code with PKCE S256.
**A human must sign in and approve in a browser.**

- No API keys, no personal access tokens, no service accounts.
- `client_credentials` is advertised in the metadata, but it carries no user identity, so it reaches no user's quota, favorites or searches. Do not use it to search.
- Register dynamically. There is no manual application form.

## Register

Open dynamic client registration, RFC 7591. No credential needed:

```http
POST https://www.halulu.food/api/mcp-auth/oauth2/register
Content-Type: application/json

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

The response carries `client_id` and, for confidential clients, `client_secret`.

## Claim

**Halulu has no claim ceremony and no agent-identity assertion.** Nothing is verified out of band.
In place of a claim step, a human signs in and approves the request in a browser:

```
GET https://www.halulu.food/api/mcp-auth/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.example/callback
  &scope=openid%20profile%20email%20offline_access
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256
  &state=RANDOM
```

`code_challenge_method` must be `S256`. Include `offline_access` or you get no refresh token and the
connection dies after one hour.

## Use the token

Exchange the code, then call the server:

```http
POST https://www.halulu.food/api/mcp-auth/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=CODE&redirect_uri=...&client_id=...&code_verifier=...
```

```http
POST https://www.halulu.food/api/mcp
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/list"}
```

Transport is MCP Streamable HTTP, JSON-RPC 2.0. Eight tools, listed in
`https://www.halulu.food/.well-known/mcp.json`.

Access tokens last 1 hour. Refresh with `grant_type=refresh_token`; refresh tokens last 30 days and
rotate on use. One search costs one credit: free accounts 3 a month, paid 30.
`halulu_search_credits` is free to call. Halulu never guesses a location; ask the user.

## Errors

Every error is JSON. Never HTML.

| Status | Body | What to do |
|---|---|---|
| 401 | `{"jsonrpc":"2.0","error":{"code":-32001,"message":"Unauthorized: Missing bearer token."},"id":null}` | Start at Discover. Do not retry unauthenticated. |
| 401 after a token | expired token | Refresh once, then retry. |
| 400 | `{"error":"invalid_request","error_description":"..."}` | Fix the request. Do not retry unchanged. |
| 400 `invalid_grant` | code or refresh token spent | Restart the authorization step. |
| 402 | quota exhausted | Stop searching. Tell the user, and offer `halulu_subscribe`. |
| 404 | markdown body naming the sitemap and llms.txt | The path does not exist. Do not retry. |

## Revocation

```http
POST https://www.halulu.food/api/mcp-auth/oauth2/revoke
Content-Type: application/x-www-form-urlencoded

token=ACCESS_OR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID
```

`https://www.halulu.food/api/mcp-auth/oauth2/introspect` reports whether a token is still active.
A user can also disconnect from their own account page. Ask the assistant to run
`halulu_clear_location` to erase the stored location pin.
