# Pinsvit agent authentication

The MCP endpoint `https://pinsvit.com/api/v1/mcp` is an OAuth 2.1 protected resource. An MCP client
that follows the authorization specification connects with no setup: the public tools work at once,
and the first protected call answers HTTP 401 with the challenge that starts sign-in.

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://pinsvit.com/.well-known/oauth-protected-resource/api/v1/mcp", scope="account"
```

## OAuth facts

- Issuer `https://pinsvit.com`; metadata at `https://pinsvit.com/.well-known/oauth-authorization-server`
  (authorization endpoint `/api/v1/oauth/authorize`, token endpoint `/api/v1/oauth/token`, JWKS `/api/v1/oauth/jwks`).
- Clients identify with a Client ID Metadata Document: `client_id` is the HTTPS URL of a JSON document
  naming itself and its `redirect_uris`. Claude, ChatGPT and VS Code publish theirs; dynamic client
  registration is not offered. A client without a document can be pre-registered by Pinsvit on request.
- Public clients only (`token_endpoint_auth_method` `none`), PKCE `S256` required, `resource` must be
  `https://pinsvit.com/api/v1/mcp`, one scope `account`. Every authorization response carries `iss`.
- Loopback redirect URIs (`http://localhost/…`, `http://127.0.0.1/…`) match with the port ignored.
- The consent page has no sign-in of its own: it picks up the session of a user signed in to pinsvit.com in
  the same browser, and sends anyone else there first (a new tab; the page notices the sign-in on its own).
  It shows the client's host, the redirect host and what the scope allows. Consent lasts 90 days.
- Access tokens are ES256 JWTs for the MCP resource, valid one hour, and carry the account's `roles`,
  `hc` and `lc` claims like a Firebase ID token would. Refresh tokens rotate on every use; a rotated
  token still works for 60 seconds so parallel refreshes do not sign the user out, and reuse after that
  ends the whole family (`invalid_grant`: reconnect).
- Blocking or deleting an account, or an admin changing its roles, ends its agent sessions at the next refresh;
  a role the app grants on its own (the user role on accepting the terms, the business role on creating a business)
  simply reaches the token at the next refresh.
- Access tokens are accepted at the MCP endpoint only; GraphQL takes no OAuth token in this version.

## Legacy bearer

A Firebase ID token for a Pinsvit user account, sent as `Authorization: Bearer`, is still accepted at
`/api/v1/mcp` and at `/api/v1/graphql`. Issuer `https://securetoken.google.com/pinsvit`, audience
`pinsvit`, one-hour lifetime, refreshed through the Firebase client that issued it. Agents that obtain
one from a signed-in Pinsvit client keep working; new integrations should use OAuth.

## Roles

- Creating content and booking need an account that accepted the Pinsvit terms in the app, which grants
  the user role. Business and admin roles are granted in the app as well; admin roles never reach an
  access token.
- The MCP staff tools (`list_my_places`, `list_place_bookings`, `list_place_booking_requests`, `accept_booking`,
  `decline_booking`, `mark_no_show`, `find_place_customers`, `record_bill_payment`) need the business role, which creating a business in the app grants to its owner. Staff
  members other than the owner cannot be added yet. A signed-in account without the role is not shown these
  tools in `tools/list`, and calling one answers `-32602` `Invalid tool name`. The role only opens the tools:
  each acts solely at a place the caller is a member of; the place-keyed ones answer `BOOKING_NOT_ALLOWED` elsewhere,
  and a `bookingId` from another place answers `BOOKING_INVALID: Booking not found` like any unknown id. Every other booking tool,
  used as staff or as a customer, needs the user role.

## Errors

- MCP: a protected tool called without a token answers HTTP 401 with the `WWW-Authenticate` challenge
  above and a JSON-RPC error `-32005` body; an expired or invalid token answers HTTP 401 with
  `error="invalid_token"`. An account without the required role answers JSON-RPC `-32005` with
  `ForbiddenException` inside HTTP 200. Tool-level refusals (validation, not found, rate limit, a booking
  refused or over a limit) are ordinary tool results with `isError: true` instead.
- Token endpoint: RFC 6749 codes (`invalid_grant` for a dead code or refresh token, `invalid_client`,
  `invalid_request`, `invalid_scope`, `invalid_target`, `unsupported_grant_type`).
- GraphQL: an invalid token is refused with HTTP 401; an operation the account may not run answers a
  GraphQL error.

## Links

- [Authorization server metadata](/.well-known/oauth-authorization-server)
- [Protected resource metadata](/.well-known/oauth-protected-resource/api/v1/mcp)
- [Auth metadata](/.well-known/auth-metadata.json)
- [MCP Manifest](/.well-known/mcp.json)
- [Agent Skill](/.well-known/agent-skills/pinsvit/SKILL.md)
- [API Catalog](/.well-known/api-catalog)
