Summary
Both dynamic registration paths create clients with developerId: null:
- RFC 7591 Dynamic Client Registration —
apps/auth-server/src/app/routes/oauth/register.ts:129
- Client ID Metadata Documents (CIMD) —
apps/auth-server/src/app/helpers/cimd.ts:274
Every developer-facing query keys on that column. GET /api/clients lists by
listByDeveloper(developerId) (routes/clients/index.ts:333), and single-client access is gated
by client.developerId !== developerId (:155). A row whose developer_id is null matches no
developer, so a dynamically registered client is invisible and unmanageable from the developer
portal — it cannot be listed, viewed, edited, deleted, or have its secret regenerated.
Why this is worth attention
CIMD is not a marginal path. README.md describes it as "the primary client-registration path
(MCP 2025-11-25)", with RFC 7591 dynamic registration as the documented fallback. So the
registration route the project positions as primary produces clients that no developer can
administer through the product's own administration surface.
The gap is currently invisible because the portal has no way to show a client it cannot query —
there is no "unowned clients" view to look wrong.
How it surfaced
While specifying a developer-facing audit feed, whose ownership predicate is the same
oauth_clients.developer_id join. Enumerating which audit events could ever reach a developer
turned up oauth.client.registered as permanently unreachable — not because of the event, but
because no row from any dynamically registered client can match the predicate at all. A
developer whose clients were all registered dynamically would see a permanently empty feed and no
explanation.
The design question underneath
This may be intentional: a client that registers itself has no authenticated developer to attribute
to, and inventing one would be wrong. If so, the gap is not developerId: null but the absence of
any other ownership signal — and the product should decide what happens to these clients rather
than leaving them orphaned by default.
Options worth weighing:
- Attribute at registration when the request carries developer authentication (DCR is open-mode
today, so this may not apply).
- A claim flow: a developer proves control of a CIMD document's origin and adopts the client.
- Realm-scoped visibility for operators, distinct from developer ownership.
- Decide explicitly that dynamic clients are unmanageable by design, and say so in the docs — which
at least stops the next person rediscovering it.
Tasks
Acceptance Criteria
References
apps/auth-server/src/app/routes/oauth/register.ts:129, apps/auth-server/src/app/helpers/cimd.ts:274 — the two null-owner creation sites
apps/auth-server/src/app/routes/clients/index.ts:155,333 — the ownership predicate and the list query
README.md — CIMD described as the primary client-registration path
Summary
Both dynamic registration paths create clients with
developerId: null:apps/auth-server/src/app/routes/oauth/register.ts:129apps/auth-server/src/app/helpers/cimd.ts:274Every developer-facing query keys on that column.
GET /api/clientslists bylistByDeveloper(developerId)(routes/clients/index.ts:333), and single-client access is gatedby
client.developerId !== developerId(:155). A row whosedeveloper_idisnullmatches nodeveloper, so a dynamically registered client is invisible and unmanageable from the developer
portal — it cannot be listed, viewed, edited, deleted, or have its secret regenerated.
Why this is worth attention
CIMD is not a marginal path.
README.mddescribes it as "the primary client-registration path(MCP 2025-11-25)", with RFC 7591 dynamic registration as the documented fallback. So the
registration route the project positions as primary produces clients that no developer can
administer through the product's own administration surface.
The gap is currently invisible because the portal has no way to show a client it cannot query —
there is no "unowned clients" view to look wrong.
How it surfaced
While specifying a developer-facing audit feed, whose ownership predicate is the same
oauth_clients.developer_idjoin. Enumerating which audit events could ever reach a developerturned up
oauth.client.registeredas permanently unreachable — not because of the event, butbecause no row from any dynamically registered client can match the predicate at all. A
developer whose clients were all registered dynamically would see a permanently empty feed and no
explanation.
The design question underneath
This may be intentional: a client that registers itself has no authenticated developer to attribute
to, and inventing one would be wrong. If so, the gap is not
developerId: nullbut the absence ofany other ownership signal — and the product should decide what happens to these clients rather
than leaving them orphaned by default.
Options worth weighing:
today, so this may not apply).
at least stops the next person rediscovering it.
Tasks
Acceptance Criteria
GET /api/clientsand any future developer-scoped query have a documented, deliberate answer fordeveloper_id IS NULLReferences
apps/auth-server/src/app/routes/oauth/register.ts:129,apps/auth-server/src/app/helpers/cimd.ts:274— the two null-owner creation sitesapps/auth-server/src/app/routes/clients/index.ts:155,333— the ownership predicate and the list queryREADME.md— CIMD described as the primary client-registration path