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¶
- Every project has a stable, instance-unique
slugand 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 namedHeyAira – <Project name>. - 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-Authenticatechallenge. The authorization server (issuer,/authorize,/token,/register) stays single and instance-wide. - 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
/mcpand 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. write_context.resourceis the address in use;server_identityreturns it asidentity.resource, together with the project's name, slug, description andmcp_url.- The shared
/mcpstays for grants already issued for it (deprecated). The Bridge and iOS app keep using it. - Projects gain a free-text
description, set by a project owner or admin through theproject_updatetool, 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.