Connector

Overview and authentication

The connector is a remote MCP server. A client authenticates once as the operator, then calls actions with ordinary JSON-RPC over HTTPS. There is one URL, one authorization server, and no API key to manage.

Server URL
https://www.happyhearaudio.com/api/mcp
Transport
Streamable HTTP, stateless JSON responses. POST for every JSON-RPC message.
Server name
happy-hear-audio version 0.1.0
Authorization
OAuth 2.1, authorization code with PKCE (S256). Bearer token in the Authorization header.
Authorization server
https://xofxhprcaobzjjttngyk.supabase.co/auth/v1
Resource metadata
https://www.happyhearaudio.com/api/mcp/oauth-protected-resource
Client registration
Dynamic (RFC 7591). Public clients, no secret.
Capabilities
tools only. No resources, no prompts, no list-changed notifications, no subscriptions.

Getting started

Three things happen the first time a client connects, and the MCP client libraries do all of them for you.

  1. The client POSTs to the server URL without a token and gets a 401 whose WWW-Authenticate header points at the resource metadata document. That document names the authorization server.
  2. The client registers itself at the authorization server (or uses the pre-registered public client below), sends the operator to sign in and approve, and exchanges the code for an access token and a refresh token.
  3. The client sends initialize, then tools/list, then calls tools with tools/call. Every request carries the bearer token.

If you are connecting an assistant rather than writing code, the operator guide at Connect an AI assistant covers Claude and ChatGPT step by step.

Resource metadata (RFC 9728)

GET https://www.happyhearaudio.com/api/mcp/oauth-protected-resource
{
  "resource": "https://www.happyhearaudio.com/api/mcp",
  "authorization_servers": [
    "https://xofxhprcaobzjjttngyk.supabase.co/auth/v1"
  ],
  "bearer_methods_supported": [
    "header"
  ]
}

Authentication

The server is a pure OAuth resource server. It verifies the bearer token against the authorization server’s published keys, reads the operator id from it, and hands every read to a database client that is limited to that operator’s rows. Nothing about the token is stored on our side.

Registering a client

Fetch https://xofxhprcaobzjjttngyk.supabase.co/auth/v1/.well-known/oauth-authorization-server for the live endpoints. It advertises a registration_endpoint, code_challenge_methods_supported including S256, and the authorization_code and refresh_token grants. Register with your redirect URI; the consent screen later shows the operator the host it will return them to, and accepts only an https redirect (or http on loopback) that the client registered.

Claude ships with a pre-registered public client, id 69891a84-9ac8-4161-9de1-2f3f5ba82485, PKCE, no secret. It identifies the Claude application to Happy Hear Audio, not the operator, who still signs in with their own login. Any other client should register itself rather than reuse it.

What the token means

  • It is scoped to one operator. There is no account-wide or partner token.
  • It must be an OAuth client token. A browser session token from the dashboard verifies but is refused with oauth_client_token_required.
  • Access tokens are short-lived (about an hour). Use the refresh token; do not cache access tokens across runs.
  • Tokens issued to a client carry a client_id claim, and the database denies every direct write to such a token. Writes happen only through the three write actions, which run server-side, scoped to the verified operator, and each append a row to the action log.
Scopes: the authorization server lists openid, profile, email and offline_access. There is no per-action scope today; the consent screen grants the connector as a whole, and the read-only / write split is enforced by the actions themselves.

Making calls

Every message is a JSON-RPC 2.0 object POSTed to the server URL with Content-Type: application/json and Accept: application/json, text/event-stream. The server answers in JSON; it never holds a stream open.

1. Initialize

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "northlight-ops-bot",
      "version": "0.3.1"
    }
  }
}

2. List tools

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

The result lists 10 tools, each with name, title, description, a JSON Schema inputSchema and annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). The same list, without connecting, is at /developers/connector/tools.json.

3. Call a tool

See the page for each action for a full example. On success the result carries the data twice: as a JSON string in content[0].text for clients that only read text, and as an object in structuredContent. Start with search_events; every other per-event action takes an id it returns.

Failures inside a tool

A refused or failed action is still a successful JSON-RPC response. The result has isError: true and one text item with a message written for the operator to read. Arguments that fail the input schema come back as a JSON-RPC error instead. Both are on the Errors page.

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "No event with that id in your account."
      }
    ],
    "isError": true
  }
}

What is never exposed

Reads go through two database views that carry an allow-list of columns, and a test fails the build if an output field is ever named after one of these. No action, under any argument, returns:

  • Guest audio or video, as a file, a storage path or a signed URL.
  • Transcript text, emotion tags or detected speaker names.
  • Guest phone numbers or text-message bodies.
  • Gallery passwords or share codes (only whether one is set), and call-in event codes.
  • Card, invoice or payment details. Payment never runs through the connector.
  • Any other operator’s data. Row-level security answers only for the token’s operator.

There is also no delete action, no action that sends a gallery or an email, and no action that changes branding, player style, call-in dates on their own or sharing settings.

Revoking access

From the operator’s side, in order of how fast each one takes effect:

  1. Remove the connector in the assistant (Claude: Customize, then Connectors; ChatGPT: Settings, then Connectors). The client stops calling at once. The grant on our side stays until it is revoked, so a copied token could still be used until it expires.
  2. Revoke the grant on our side. The authorization server supports the standard grant revocation as the signed-in user. If a client does not surface it, the operator replies to any Happy Hear Audio email and we revoke the grant for that client by hand: the consent is marked revoked and that client’s sessions and refresh tokens are deleted, which stops renewal. The operator’s normal dashboard login is untouched.
  3. An access token already issued keeps working until its expiry (about an hour). Deleting the sessions prevents it being refreshed.

Reconnecting later is the same consent screen again.