Skip to content

ADR-0003: One project = one address

  • Status: accepted
  • Date: 2026-09-26
  • Task: 7d466a59-9d1c-419d-bdbd-385972c0b7bd

Context

The MCP resource was one URL for the whole instance, https://mcp.heyaira.eu/mcp. Clients that key a connector by URL (claude.ai refuses a second connector with the same URL in one organization; ChatGPT behaves similarly) can hold only one grant per URL, so a person working on two projects could not connect both in the same client. The project was also invisible at connection time: nothing in the address or the consent step said which project was being connected.

Decision

  1. Every project has a stable, instance-unique slug and its own MCP resource, https://<host>/p/<slug>/mcp, with its own RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource/p/<slug>/mcp (resource_name: HeyAira – <Project name>). Connectors are named HeyAira – <Project name>.
  2. Per-project paths, not per-project hosts. Paths need no wildcard DNS or certificates, the existing edge and reverse proxy already forward every path, and MCP clients discover path-based metadata from the WWW-Authenticate challenge. The authorization server (issuer, /authorize, /token, /register) stays single and instance-wide.
  3. A grant is bound to the address it was issued for. On an MCP request an OAuth token is accepted only on its stored resource, and only while that address still belongs to the token's project. A static project token is accepted on the legacy /mcp and on its own project's address, never on another project's. Consent names the project from the address and refuses a project access token of any other project.
  4. write_context.resource is the address in use; server_identity returns it as identity.resource, together with the project's name, slug, description and mcp_url.
  5. The shared /mcp stays for grants already issued for it (deprecated). The Bridge and iOS app keep using it.
  6. Projects gain a free-text description, set by a project owner or admin through the project_update tool, so every session sees what the project is about. Name and slug remain operator settings (heyaira-admin).

Implementation

The MCP SDK checks one resource_server_url. HeyAira keeps a single MCP app and adds ProjectResourceRouter as the outermost ASGI middleware: it resolves /p/<slug>/mcp to the project, forwards to the shared handler, records the address in a request context variable and rewrites the metadata URL in the WWW-Authenticate challenge. OAuthProvider.load_access_token enforces the binding against that address, so the SDK's own single-resource check is disabled (validate_token_resource=False). Migration 024 adds projects.slug and projects.description.

Consequences

  • One client can hold one connector per project; the URL and the consent page show the project.
  • A slug is part of every grant's binding. Changing it (--replace-slug) invalidates grants for the old address; clients must reconnect.
  • The router must stay the outermost layer: without it MCP requests would not carry an address and the binding check would not apply.