---
id: w4/m9/t004
title: Agent discovery — Hydra DCR enabled + bex-api RFC 9728 protected-resource metadata
worker: worker4
status: done
estimate: 75m
depends_on: [w4/m9/t001]
---

## Objective

Make bex discoverable and joinable by MCP clients per the MCP authorization
spec: enable **Dynamic Client Registration** on Hydra (agents self-register —
no seeding scripts), and have bex-api advertise **RFC 9728 OAuth protected
resource metadata** so a client that hits `/mcp` unauthenticated can find the
authorization server on its own.

## Context

- MCP authorization spec = OAuth 2.1 + RFC 8414 (AS metadata, Hydra already
  serves `/.well-known/…`) + RFC 7591 DCR + RFC 9728 (resource metadata).
- Hydra DCR: enable via `oidc.dynamic_client_registration.enabled: true` in
  `hydra.values.yaml`; the public endpoint path is version-dependent
  (`/oauth2/register` on current Hydra, `/connect/register` on older) — confirm
  against the deployed version. Registered agents are NOT auto-trusted for
  consent (t003 denies unknown clients until a consent UI exists — spec-compliant).
- **RFC 8707 resource indicators (MCP MUST)**: MCP clients send a `resource`
  parameter on authorize + token requests, and the MCP server MUST validate the
  token's audience. Hydra natively speaks its own `audience` parameter — verify
  how it handles `resource` (map, ignore, or reject) and record the finding.
  bex-api's introspection currently checks only `active`: add an audience check
  (introspected `aud` vs the MCP server's canonical URI) when `BEX_OAUTH_ISSUER`
  is set, per MCP spec §token-validation.
- bex-api: serve `GET /.well-known/oauth-protected-resource` (unauthenticated,
  next to `/healthz` in `lego/backend/internal/api/server.go`) naming the Hydra
  issuer; add `WWW-Authenticate: Bearer resource_metadata="…"` to `/mcp` 401s
  (`unauthorized()` today sets bare `Bearer`). New env for the issuer URL
  (e.g. `BEX_OAUTH_ISSUER`; unset ⇒ endpoint 404s and 401s stay as today).
- Keep three-adapter parity rules in mind: this is transport-level wiring in
  the composition root, not a feature verb.

## Steps

1. Enable DCR in `deploy/gitops/base/values/hydra.values.yaml` (+ local overlay); verify `POST /oauth2/register` creates a public PKCE client.
2. Add the protected-resource metadata endpoint + enriched `WWW-Authenticate` in bex-api behind `BEX_OAUTH_ISSUER`; mirror the env into `lego/operator/config/api/deployment.yaml` and `.env.example`/`.env.template`.
3. Unit-test the metadata endpoint + header (Go).

## Files

- `deploy/gitops/base/values/hydra.values.yaml`, `deploy/gitops/overlays/local/values/hydra.values.yaml`
- `lego/backend/internal/api/server.go` (+ small metadata handler + tests)
- `lego/operator/config/api/deployment.yaml`, `.env.example`, `.env.template`

## Acceptance criteria

- [x] Hydra's public DCR endpoint (`/oauth2/register`; `/connect/register` on older versions — confirm deployed path) self-registers a public PKCE client with no admin credential.
- [x] `GET <bex-api>/.well-known/oauth-protected-resource` returns the issuer; `/mcp` 401s carry `resource_metadata`.
- [x] Hydra's handling of the RFC 8707 `resource` parameter is verified and recorded; bex-api validates the introspected token's audience when configured.
- [x] Unset `BEX_OAUTH_ISSUER` leaves today's behavior byte-identical.

## Out of scope

- Scope-based authorization of MCP tools (OpenFGA already gates verbs); consent UI.
