diff --git a/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-CONTEXT.md b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-CONTEXT.md
new file mode 100644
index 00000000..e6648091
--- /dev/null
+++ b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-CONTEXT.md
@@ -0,0 +1,124 @@
+# Phase 32.1: MCP OAuth for plugin apps: declared scopes, MCP subject on context, per-client MCP consent - Context
+
+**Gathered:** 2026-10-28
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+An agent connected to `/mcp` over OAuth gets the same tools, attribution and revocation as one using a member personal API token, so that a consuming application can switch OAuth sign-in on in production. Three additive gaps are closed:
+
+1. **Declared scopes over MCP OAuth.** Scopes a plugin declares through `pact.HasFrontendTokenScopes` can be requested, consented to and granted on the MCP OAuth server, and appear in pier's protected-resource metadata. Today the list is fixed to `read`, `write`, `ai`, `offline_access` (+ `backend` on the MCP server).
+2. **MCP call context.** A route run by an MCP tool call can read who called it (guard, user, client ID, token id, scopes) and the MCP session through a `pact` accessor.
+3. **MCP consent row.** `sm-oauthserver-plugin` records a consent for MCP grants, so the existing per-client revoke ends one agent's grant and its MCP sessions.
+
+Everything ships as patch releases: framework 0.5.x, `sm-oauthserver-plugin`, and `sm-user-plugin` (it publishes the scope catalog). Module READMEs and `docs/` pages describe the changes.
+
+The source of the three gaps is a consuming app's agent-access work (its D-36, D-37, D-38). Those notes were not found in the local sibling repos; the ROADMAP entry is the authoritative statement.
+
+
+
+
+## Implementation Decisions
+
+### Declared scopes and the read/write ceiling
+- **D-01:** **The 19.7 D-20 ceiling stays.** OAuth `read`/`write` remain the ceiling: a non-read-only tool still needs `write`, and a route's `user.scope:` still needs the declared scope in the grant. A write tool behind `user.scope:bm:write` needs both `write` and `bm:write`, as a personal token with read/write plus declared scopes does today. No pier ceiling change.
+- **D-02:** **Declared scopes are offered on the MCP OAuth server only**, like `backend` (19.7 D-21). The OIDC server keeps its PHP-parity scope list.
+- **D-03:** **Advertised scopes = fixed scopes + declared scopes, filtered by an optional opt-in list.** When the config list is set, only the declared scopes it names are advertised and accepted. When it is unset, every declared scope is. The fixed scopes (`read`, `write`, `ai`, `offline_access`, `backend`) are always present. The same set drives the wristband MCP server's metadata `scopes_supported`, its authorize allow-list (`allowedAuthorizeScopes`) and pier's PRM `scopes_supported`. This rule must be documented clearly in the oauthserver README, the pier README and `docs/services/oauth-server.md` / `docs/services/mcp.md`. A name in the list that no plugin declares should be a clear boot error or warning; the planner decides which. — **Reversibility:** costly — scope names published in metadata and stored in grants.
+- **D-04:** **The config key lives in sm-oauthserver-plugin** (proposed `golem15.oauthserver.mcp_scopes`), next to where `NewServers` builds `mcpOpts.ScopesSupported`. **pier reads the list from the published verifier** (`pact.MCPTokenVerifier`, extended additively) instead of its own `PRMScopes` constant, so the PRM and the authorization server cannot drift. `pier.PRMScopes` stays as the fallback when no verifier publishes a list.
+- **D-05:** **Plumbing: a published pact catalog.** The plugin that owns member tokens (sm-user-plugin, which already compiles and validates `FrontendTokenScopes()` at boot) publishes the compiled list on the app (`app.Lookup[...]`) through a new additive pact interface. sm-oauthserver-plugin and pier read it at boot. Labels and descriptions travel with the names so the consent page can show them.
+
+### Consent page
+- **D-06:** **Per-scope checkboxes.** This changes the consent behaviour from 19.4's allow/deny-the-whole-request. Every requested scope starts ticked. `read`, when requested, is locked (cannot be unticked). `offline_access`, `backend` and declared scopes can be unticked. A submit that leaves no data-bearing scope keeps the existing 422 `no_grantable_scopes`. `MCPConsentDecide` already intersects submitted ∩ requested (`grantedScopes`), so the server side mostly exists.
+- **D-07:** **Both surfaces.** The MCP consent JSON (show answer of `controllers/mcp_consent_api.go`) carries a label and description per requested scope: the FrontendTokenScope label/description resolved through phrasebook for declared scopes, existing text for fixed scopes. It also says whether each scope is locked, so redirect-mode app pages can render checkboxes or keep all-or-nothing (as the consuming app chose in its D-07). The built-in consent page renders the checkboxes. *Research must confirm which built-in page the MCP flow uses today* (`views/consent.html.tmpl` vs redirect-only to an app `/connect` page). If the MCP flow has no built-in page, the checkboxes go wherever the plugin ships its MCP consent rendering, and the planner raises it.
+- **D-08:** **Re-consent on scope growth.** When a client that already holds a consent requests any scope the recorded consent does not cover, the user goes through the consent page again. After the decision, the stored consent's scopes become the newly granted set.
+
+### MCP call context accessor
+- **D-09:** **The accessor lives in `pact`:** `pact.MCPCallFrom(ctx) (MCPCall, bool)` with a matching setter that pier calls next to `bouncer.WithDispatch` in `dispatch.go`. Plugins already import pact, so they need no pier or bouncer import.
+- **D-10:** **New `pact.MCPCall` wrapper:** `MCPCall{Subject MCPSubject, SessionID string, Tool string}`. `MCPSubject` (the guard-facing type) stays unchanged. `SessionID` is the MCP session id and is empty in pier's stateless mode. `Tool` is the tool name. Exact field names are the planner's call within this shape. — **Reversibility:** costly — exported pact API that consuming apps will read.
+- **D-11:** **Every MCP tool call carries it:** OAuth and personal-token, frontend and backend guards. The subject is the one pier resolved for that tool's guard (`id.subjectFor(t.guardName)`). Plain HTTP requests have none (`ok == false`). A personal-token call's credential keeps carrying the token id as today (sm-user-plugin v0.1.14); the accessor is additive to it, not a replacement.
+
+### MCP consent row and revoke
+- **D-12:** **The same `golem15_oauthserver_consents` table.** When an MCP code is issued (`MCPConsentDecide` → `IssueCode`, and any direct grant path through the MCP store), upsert the (user_id, client_id) row with the granted scopes. MCP clients are DCR clients distinct from OIDC clients, so the unique (user_id, client_id) constraint does not collide. The existing `revoke` (deletes the consent, plus codes, access and refresh tokens of the pair, then emits `MCPSubjectRevoked{ClientID}`, which pier uses to evict sessions) then works unchanged for MCP clients.
+- **D-13:** **MCP clients appear in `GET /oauth/authorizations`** with an additive kind marker (e.g. `"kind":"mcp"`; OIDC entries get `"oidc"` or omit it, per the planner). Existing fields and order are unchanged.
+- **D-14:** **A data migration backfills consents** for grants that predate this release: one row per (user, client) with live MCP access or refresh tokens, scopes = union of the tokens' scopes, `created_at` = earliest token. It must be idempotent and skip pairs that already have a consent. — **Reversibility:** one-way — writes rows into a shared table on every consuming install.
+- **D-15:** **One revoke ends everything for that client:** frontend and backend (`backend_user_id`) access and every MCP session of the client. No separate backend revoke.
+
+### Release
+- **D-16:** Patch releases only: framework 0.5.x (never hardcode the version), `sm-user-plugin` (catalog publication), `sm-oauthserver-plugin` (scopes, consent JSON/page, consent row, backfill, listing marker). Commit and push each plugin before bumping pointers in apps. Each pushed framework tag gets a changelog post.
+
+### Claude's Discretion
+- The pact interface name and shape for the published scope catalog (D-05), within "names + label + description, additive".
+- Whether an opt-in name nobody declares fails boot or warns (D-03).
+- Field names of `MCPCall` and the setter name (D-10).
+- How the kind marker is spelled in the authorizations listing (D-13).
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Phase scope
+- `.planning/ROADMAP.md` § "Phase 32.1" — goal and the four success criteria.
+
+### Prior decisions this phase builds on
+- `.planning/phases/19.7-generic-mcp-module/19.7-CONTEXT.md` — pier design; D-10 (tool binds to route, in-process dispatch), D-16 (permissions from route middleware), D-20 (read/write ceiling, kept by D-01), D-21 (`backend` scope on the MCP server only).
+- `.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md` — sm-oauthserver-plugin's two-Server design, client kinds, consent model, additive wristband interfaces.
+- `../sm-bm-app/.planning/phases/03.3-mcp-access-for-bm/03.3-CONTEXT.md` — a consuming app's MCP access decisions (D-07 consent page all-or-nothing in that app; personal tokens alongside OAuth).
+
+### Docs to update
+- `modules/pier/README.md`, `modules/wristband/README.md`, `modules/pact/README.md`
+- `docs/services/mcp.md`, `docs/services/oauth-server.md`, `docs/services/authentication.md`
+- `../sm-oauthserver-plugin/README.md`, the sm-user-plugin README
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `modules/pact/capabilities.go:821-835`: `FrontendTokenScope{Name, Label, Description}` and `HasFrontendTokenScopes`; `ValidTokenScopeName`.
+- `modules/pact/mcp.go:134`: `MCPSubject{Guard, UserID, Scopes, ClientID, TokenID}`, wrapped by the new `MCPCall`.
+- `modules/pact/mcp.go:289-303`: `MCPSubjectChanged` with `MCPSubjectRevoked` + `ClientID`, which pier already uses to evict one client's sessions (`modules/pier/events.go:75-90`).
+- `../sm-oauthserver-plugin/controllers/mcp_consent_api.go`: `MCPConsentDecide` and `grantedScopes` (submitted ∩ requested) already support partial grants.
+- `../sm-oauthserver-plugin/controllers/authorizations.go`: `AuthorizationsIndex` and `revoke(userID, clientID)` (consent + codes + access/refresh tokens in one tx, then the revoked event).
+
+### Established Patterns
+- Scope lists are hardcoded today: `modules/wristband/server.go:99` (`ScopesSupported`), `modules/wristband/authorize.go:24` (`authorizeAllowedScopes`, used only when `Options.ScopesSupported` is empty), `modules/pier/prm.go:14` (`PRMScopes`).
+- `../sm-oauthserver-plugin/servers.go:158` builds the MCP server's scopes as OIDC scopes + `backend`; the declared/opt-in set goes in here.
+- `modules/pier/filter.go:60-70`: the read/write ceiling (`id.writes()`) before the guard's `MCPAllows`; unchanged by D-01.
+- sm-user-plugin `classes/mcp_guard.go`: `MCPAllows` requires the route's `user.scope` names in `subject.Scopes`; the personal-token `MCPDispatch` builds an in-memory credential carrying the token.
+- Find-or-publish of shared registries on `backpack.App` (`pact.MCPGuards`) is the model for the published scope catalog.
+
+### Integration Points
+- `modules/pier/dispatch.go:177-200`: where `bouncer.WithDispatch` is set; the `MCPCall` setter goes next to it (session id from the call info, tool from `t.name`).
+- `modules/pier/prm.go:mountPRM`: already looks up `pact.MCPTokenVerifier`; extend it to read the scope list from there.
+- `../sm-oauthserver-plugin/classes/mcp_store.go`: the MCP grant/token store; the consent upsert hooks in at code issue (and at direct grant minting).
+- `../sm-oauthserver-plugin/updates/`: new migration for the backfill (D-14).
+
+
+
+
+## Specific Ideas
+
+- The opt-in-or-all rule (D-03) must be documented with examples: unset = all declared scopes, set = only those listed, fixed scopes always.
+- A redirect-mode app may keep an all-or-nothing consent page; the JSON must give it enough (label, description, locked) to render either way.
+
+
+
+
+## Deferred Ideas
+
+- Offering declared scopes on the OIDC server (non-MCP OAuth clients). Rejected for this phase (D-02).
+- Release/rollout order across framework, sm-user-plugin, sm-oauthserver-plugin and the consuming app was not discussed in detail; the planner follows the standard "plugin first, pointer bump second" order.
+
+### Reviewed Todos (not folded)
+- 27 pending todos matched on keywords only (e.g. "Record PHP OIDC parity against WavePath", user-plugin logout cookie, activate-by-code second factor). None touch MCP scopes, call context or MCP consent; none were folded.
+
+
+
+---
+
+*Phase: 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con*
diff --git a/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-DISCUSSION-LOG.md b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-DISCUSSION-LOG.md
new file mode 100644
index 00000000..1d63f5b2
--- /dev/null
+++ b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-DISCUSSION-LOG.md
@@ -0,0 +1,126 @@
+# Phase 32.1: MCP OAuth for plugin apps - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-10-28
+**Phase:** 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con
+**Areas discussed:** Declared scopes vs ceiling, Consent page for scopes, MCP subject accessor, MCP consent row & revoke
+
+---
+
+## Declared scopes vs ceiling
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Keep both | D-20 read/write ceiling stays; declared scopes are extra gates | ✓ |
+| Declared scope stands alone | Tools behind a declared scope skip the read/write ceiling | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Fixed + all declared | Every FrontendTokenScope is advertised | |
+| Fixed + config opt-in list | Only listed declared scopes advertised | |
+| Other | | ✓ |
+
+**User's choice:** Opt-in config list if set; if unset, all declared scopes. "Document this properly."
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| MCP server only | Same as `backend` (D-21) | ✓ |
+| Both servers | OIDC clients could request plugin scopes too | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| You decide | Researcher/planner pick the mechanism | |
+| Published pact catalog | User plugin publishes the compiled list; oauthserver and pier read it | ✓ |
+
+---
+
+## Consent page for scopes
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| oauthserver, pier reads it | `golem15.oauthserver.mcp_scopes`; pier reads from the verifier | ✓ |
+| pier (mcp.scopes) | MCP module owns the list | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Label+description, all-or-nothing | As in 19.4 | |
+| Per-scope checkboxes | User can untick individual scopes | ✓ |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Both: JSON + built-in page | Consent JSON carries label/description; built-in page renders them | ✓ |
+| JSON only | Built-in page shows raw names | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Show consent again | Any uncovered requested scope sends the user through consent | ✓ |
+| You decide | Follow the OIDC path | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| All ticked, read locked | `read` can't be unticked; others can | ✓ |
+| All ticked, none locked | Anything can be unticked | |
+
+---
+
+## MCP subject accessor
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| pact | `pact.MCPSubjectFrom(ctx)` | ✓ |
+| pier | `pier.SubjectFrom(ctx)` | |
+| bouncer | Subject field on `bouncer.Dispatch` | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Every MCP tool call | OAuth + PAT, frontend + backend | ✓ |
+| OAuth calls only | | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| MCPSubject as is | Guard, UserID, ClientID, TokenID, Scopes | |
+| Plus the MCP session id | Also Mcp-Session-Id | ✓ |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| New pact.MCPCall wrapper | `MCPCall{Subject, SessionID, Tool}`; MCPSubject unchanged | ✓ |
+| SessionID field on MCPSubject | Guards see the field too | |
+
+---
+
+## MCP consent row & revoke
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Same consents table | Upsert (user, client) at MCP code issue | ✓ |
+| Separate MCP consents table | New table, revoke/listing look in both | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Yes, with a kind marker | Additive `"kind":"mcp"` | ✓ |
+| Yes, no new field | | |
+| Separate MCP list endpoint | | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Migration backfills rows | From live MCP access/refresh tokens | ✓ |
+| Lazy: write on next refresh | | |
+| Nothing | | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Yes, one revoke ends all | Frontend + backend + sessions | ✓ |
+| You decide | | |
+
+---
+
+## Claude's Discretion
+
+- Pact catalog interface name/shape; boot error vs warning for unknown opt-in names; `MCPCall` field and setter names; kind marker spelling.
+
+## Deferred Ideas
+
+- Declared scopes on the OIDC server.
+- Detailed rollout order across repos (planner uses the standard plugin-first order).