From eadd58414d5bb546db73ac7dbc894fae5f0d2489 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 23 Oct 2026 00:23:08 +0200 Subject: [PATCH] docs(19.7-02): close plan without a release --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 16 +- .../19.7-02-SUMMARY.md | 271 ++++++++++++++++++ 3 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/19.7-generic-mcp-module/19.7-02-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b93e9697..acc85a01 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1454,14 +1454,14 @@ Plans: 3. `tools/list` lists only the tools the session's principals may call (frontend permissions through the merged group/user permissions, backend permissions and token scope through cabana), and a call runs through the route's own middleware. 4. An editor signs in once through `wristband` OAuth as a frontend user; with a verified link to a backend user, backend-guarded tools such as Journal's run as that admin, and removing the link or the grant removes them. The phase gets a security review; the MCP SDK dependency has a decision note before code lands; unit tests are the last plan. -**Plans:** 1/5 plans executed +**Plans:** 2/5 plans executed Plans: **Wave 1** - [x] 19.7-01-PLAN.md — Framework seams: bouncer dispatch marker, surf handler lookup and streaming routes, tagged pier mount hook, SDK-free pact MCP contracts, My account contribution seam, prompted record action, wristband scope set **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 19.7-02-PLAN.md — The `pier` module behind `summercms_mcp` (go-sdk, hybrid stateful/stateless, Redis sessions, fan-out, PRM) and the framework release carrying 19.6.1 + 19.7-01/02 +- [x] 19.7-02-PLAN.md — The `pier` module behind `summercms_mcp` (go-sdk, hybrid stateful/stateless, Redis sessions, fan-out, PRM) and the framework release carrying 19.6.1 + 19.7-01/02 **Wave 3** *(blocked on Wave 2 completion)* - [ ] 19.7-03-PLAN.md — sm-user-plugin: verified account link, Administrators-screen link actions, plugin frontend permission codes, the `user` MCP guard and `user_me` tool, plugin release and app pointer bumps @@ -1607,7 +1607,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 19.5. Social login in sm-user-plugin | 5/5 | Complete | 2026-10-21 | | 19.6. Two-factor auth and device flow in sm-user-plugin | 0/TBD | Not started | - | | 19.6.1. Backend Administrators screen (INSERTED) | 5/5 | Complete | 2026-10-22 | -| 19.7. Generic MCP module | 1/5 | In Progress| | +| 19.7. Generic MCP module | 2/5 | In Progress| | | 19.8. Console commands overhaul (INSERTED) | 6/6 | In Progress| | | 20. Demo instance with every shared plugin | 0/TBD | Not started | - | | 30. Płytarium cutover | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index b9333884..168745e2 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: "19.7" current_phase_name: Generic MCP module (INSERTED) status: executing -stopped_at: Completed 19.7-01-PLAN.md -last_updated: "2026-10-22T22:15:45.219Z" +stopped_at: Completed 19.7-02-PLAN.md +last_updated: "2026-10-22T22:22:58.311Z" last_activity: 2026-10-23 last_activity_desc: "Completed quick task 261022-x7a: user-plugin golem15.user.password.public_routes switch (login/forgot/reset 404 when false)" -state_head: e5dd4579a8a1503b9711fa27bc140e5b594baa88 +state_head: a26ee2b72d5b99db7596217ce1c3da7ec0c6bf89 progress: total_phases: 45 completed_phases: 37 total_plans: 238 - completed_plans: 230 + completed_plans: 231 gsd_state_version: "1.0" milestone_name: milestone --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-22) ## Current Position Phase: 19.7 (Generic MCP module (INSERTED)) — EXECUTING -Plan: 2 of 5 +Plan: 3 of 5 Status: Ready to execute Last activity: 2026-10-23 — Completed quick task 261022-x7a: user-plugin golem15.user.password.public_routes switch (login/forgot/reset 404 when false) @@ -277,6 +277,7 @@ Progress: [████████░░] 82% | Phase 19.6.1 P04 | 18 min | 3 tasks | 10 files | | Phase 19.6.1 P05 | 4 min | 2 tasks | 7 files | | Phase 19.7 P01 | 52 min | 5 tasks | 64 files | +| Phase 19.7 P02 | 38 min | 4 tasks | 36 files | ## Accumulated Context @@ -948,6 +949,7 @@ Recent decisions affecting current work: - [Phase 19.7]: Framework release is v0.5.0 (19.7-02 Task 5), carrying 19.6.1, 19.7 plans 01-02 and a set of 7 msd-quick patches; tag only after all 7 are committed — User decision 2026-10-22: minor bump for the new pier module and pact capabilities; hold the tag until the quick patches land - [Phase 19.7]: v0.5.0 is tagged only when phase 19.7 is fully complete (plans 01-05 and phase verification); supersedes the earlier 19.7-02 Task 5 entry. Plan 02 closes without a tag; no intermediate framework tag for 19.7 work — User decision 2026-10-23: a minor version marks the finished MCP phase, not a partial one - [Phase 19.7]: Once the user confirms all in-flight msd-quick tasks are finished: release the framework as the next v0.4.x patch (carrying the quick patches and 19.7 work so far) and tag the held user-plugin fixes; plugins may be tagged mid-phase. Framework v0.5.0 stays reserved for full MCP usability at phase 19.7 completion — User decision 2026-10-23: plugins can be tagged now, framework 0.5 means the MCP phase is finished; wait for the user's word that the last quick agent is done +- [Phase 19.7]: 19.7-02: pier serves 2025-11-25 clients through its own session router over StreamableServerTransport (Redis-shared sessions, D-25) and 2026-07-28 clients through the SDK stateless handler; plan 02 closed without a tag, pier ships opt-in in v0.4.4 (summercms_mcp tag plus mcp.enabled, default off) ### Pending Todos @@ -1063,6 +1065,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-22T14:44:54.330Z -Stopped at: Completed 19.7-01-PLAN.md +Last session: 2026-10-22T22:22:57.128Z +Stopped at: Completed 19.7-02-PLAN.md Resume file: None diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-02-SUMMARY.md b/.planning/phases/19.7-generic-mcp-module/19.7-02-SUMMARY.md new file mode 100644 index 00000000..72f22359 --- /dev/null +++ b/.planning/phases/19.7-generic-mcp-module/19.7-02-SUMMARY.md @@ -0,0 +1,271 @@ +--- +phase: 19.7-generic-mcp-module +plan: 02 +subsystem: mcp +tags: [pier, mcp, go-sdk, jsonschema-go, surf, bouncer, redis, sessions, stateless, prm, build-tag] + +requires: + - phase: 19.7-01 + provides: bouncer.WithDispatch marker, surf.Router.Handler + MountStream + mountPier hook, cabana.ResolveBackendToken / ScopeLimitFor / BackendPrincipal, SDK-free pact MCP contracts, pact.MCPSubjectChanged events, cooler.KeyPrefix, wristband ScopesSupported +provides: + - modules/pier, the only importer of go-sdk v1.8.0 and jsonschema-go v0.4.3, linked only into binaries built with -tags summercms_mcp + - modules/surf/pier_on.go (//go:build summercms_mcp) adapting the router to pier.Host + - catalog compiled at boot from pact.HasMCP (_ prefixing, route binding, guard resolution, schema reflection, annotations, boot failures by name) + - in-process tool dispatch through surf's compiled chain with the bouncer marker and no Authorization or Cookie header + - per-request identity (OAuth verifier, personal tokens behind mcp.allow_personal_tokens, backend scope and link), read/write ceiling, built-in backend guard provider + - stateful 2025-11-25 sessions over a memory or Redis store with rehydration, binding checks, DELETE and pub/sub fan-out; stateless 2026-07-28 path on the same mcp.path + - RFC 9728 protected resource metadata and WWW-Authenticate challenge; periodic re-verification of long GET streams + - prompts/get, audit lines (pier: tool call, pier: session) + - docs/services/mcp.md, modules/pier/README.md, root README row, decision note .planning/notes/2026-10-22-mcp-go-sdk.md and STACK.md rows +affects: [19.7-03 sm-user-plugin link and frontend permissions, 19.7-04 oauthserver verifier and Journal, 19.7-05 unit tests and security review, framework v0.4.4 (pier opt-in), framework v0.5.0 at phase completion] + +actuals: + tokens: 51272 + tasks: 4 + commits: 29 +plan_head_before: 3746675c726909b9280e78ea7f73985a1dcb48a5 +plan_head_after: a26ee2b72d5b99db7596217ce1c3da7ec0c6bf89 + +tech-stack: + added: + - "github.com/modelcontextprotocol/go-sdk v1.8.0 (tagged builds only)" + - "github.com/google/jsonschema-go v0.4.3 (tagged builds only)" + patterns: + - "Build-tag containment: the SDK is reachable only through modules/pier and modules/surf/pier_on.go; an untagged binary lists no modelcontextprotocol module" + - "One SDK server per identity; a receiving middleware re-filters tools/list and tools/call for the current request's identity" + - "pier's own session router over StreamableServerTransport so sessions can be rehydrated from a shared Redis store" + - "Identity re-read from the bearer on every request; the stored session carries only the binding, never the token" + - "Store opened lazily from redis.url under cooler's prefix; nothing dials at mount" + +key-files: + created: + - .planning/notes/2026-10-22-mcp-go-sdk.md + - modules/pier/README.md + - modules/pier/pier.go + - modules/pier/config.go + - modules/pier/host.go + - modules/pier/catalog.go + - modules/pier/dispatch.go + - modules/pier/filter.go + - modules/pier/identity.go + - modules/pier/guards.go + - modules/pier/backend_guard.go + - modules/pier/session.go + - modules/pier/stateless.go + - modules/pier/store.go + - modules/pier/store_memory.go + - modules/pier/store_redis.go + - modules/pier/events.go + - modules/pier/prm.go + - modules/pier/audit.go + - modules/pier/export_test.go + - modules/pier/example_mcp_test.go + - modules/pier/tracer_smoke_test.go + - modules/pier/identity_smoke_test.go + - modules/pier/sessions_smoke_test.go + - modules/pier/stateless_smoke_test.go + - modules/surf/pier_on.go + - docs/services/mcp.md + modified: + - go.mod + - go.sum + - .planning/research/STACK.md + - README.md + - modules/surf/README.md + - docs/site.yaml + - docs/services/authentication.md + - docs/plugins/registration.md + - .planning/phases/19.7-generic-mcp-module/deferred-items.md + +key-decisions: + - "D-24: go-sdk v1.8.0 and jsonschema-go v0.4.3 chosen and approved by the user (Task 1 legitimacy check) before any go get; decision note and STACK rows committed first (3ea82a1)" + - "Stateful 2025-11-25 sessions are served by pier's own router over StreamableServerTransport (needed for Redis-shared sessions, D-25); 2026-07-28 clients go through the SDK's stateless handler; both share identity, dispatch and filtering (D-32)" + - "The mounted *pier.Pier is published on the application so tests reach it through export_test.go seams" + - "Release deferred: no tag in this plan. pier ships opt-in in framework v0.4.4 (build tag summercms_mcp plus mcp.enabled, default off), released by the orchestrator outside this plan; v0.5.0 is reserved for phase 19.7 completion (user decision 2026-10-23)" + +patterns-established: + - "Tracer first: one fake-plugin tool listed and called end to end by an SDK client through the stateful path before Redis, stateless and identity expansion" + - "Docs example as a test: TestExampleBlogPlugin builds the acme.blog declaration that docs/services/mcp.md copies through src=" + +requirements-completed: [SC-1, SC-2, SC-3, SC-4, D-08, D-10, D-11, D-12, D-13, D-14, D-15, D-16, D-17, D-19, D-20, D-21, D-22, D-23, D-24, D-25, D-26, D-27, D-28, D-29, D-30, D-32, D-33, D-35, D-36] + +coverage: + - id: D1 + description: "Tracer: an SDK client lists and calls acme_echo end to end through surf's compiled guard chain with the dispatch marker; the inner request carries no Authorization or Cookie header; mcp.enabled off mounts nothing; untagged binaries carry no SDK" + requirement: "SC-1" + verification: + - kind: unit + ref: "modules/pier/tracer_smoke_test.go#TestPierTracerSmoke" + status: pass + - kind: other + ref: "go build -tags summercms_mcp ./..." + status: pass + human_judgment: false + - id: D2 + description: "Catalog: _ prefixing, route binding, guard resolution, schema reflection, boot failures by name" + requirement: "SC-2" + verification: + - kind: unit + ref: "modules/pier/tracer_smoke_test.go#TestPierTracerSmoke" + status: pass + - kind: unit + ref: "modules/pier/example_mcp_test.go#TestExampleBlogPlugin" + status: pass + human_judgment: false + - id: D3 + description: "Per-identity tools/list and tools/call filtering, read/write ceiling, personal tokens behind the flag, prompts/get, audit line without arguments or token" + requirement: "SC-3" + verification: + - kind: unit + ref: "modules/pier/identity_smoke_test.go (subtests of TestPierTracerSmoke: read-write ceiling and prompts, audit line, personal tokens behind the flag)" + status: pass + human_judgment: false + - id: D4 + description: "Redis sessions: binding check (403), rehydration on another instance, DELETE ends the session everywhere" + requirement: "D-25" + verification: + - kind: unit + ref: "modules/pier/sessions_smoke_test.go#TestPierSessionsSmoke" + status: pass + human_judgment: false + - id: D5 + description: "Subject-changed fan-out: list_changed on permission and link changes, session close on revocation and deletion, across instances" + requirement: "D-26" + verification: + - kind: unit + ref: "modules/pier/sessions_smoke_test.go#TestPierFanOutSmoke" + status: pass + human_judgment: false + - id: D6 + description: "2026-07-28 clients served statelessly from the same path" + requirement: "D-32" + verification: + - kind: unit + ref: "modules/pier/stateless_smoke_test.go#TestPierStatelessSmoke" + status: pass + human_judgment: false + - id: D7 + description: "Framework tag, push and changelog post (D-31)" + requirement: "D-31" + verification: + - kind: other + ref: "deferred by user decision 2026-10-23: v0.4.4 released by the orchestrator outside this plan; v0.5.0 at phase 19.7 completion" + status: deferred + human_judgment: true + rationale: "The user reserved v0.5.0 for full MCP usability; the release is not part of this plan's close" + +duration: 38 min (tasks 1-4, 15:58Z-16:36Z; the release checkpoint wait is excluded) +completed: 2026-10-23 +status: complete +--- + +# Phase 19.7 Plan 02: pier MCP module Summary + +**The `pier` module serves an opt-in MCP endpoint on go-sdk v1.8.0 behind the `summercms_mcp` build tag. Plugin tools declared through `pact.HasMCP` are listed per identity and called in-process through surf's compiled route chains with the bouncer dispatch marker. Clients on protocol 2025-11-25 get stateful sessions over a memory or Redis store, with fan-out across instances; 2026-07-28 clients are served statelessly from the same path. The module also serves RFC 9728 resource metadata. No release was cut in this plan.** + +## Performance + +- **Duration:** 38 min of task work (2026-10-22T15:58:41Z to the Task 4 commit at 16:36Z), followed by the release checkpoint +- **Started:** 2026-10-22T15:58:41Z +- **Completed:** 2026-10-23 (closed without a release) +- **Tasks:** 4 executed, plus the release decision (Task 5, answered: no tag in this plan). Task 6 (the release) was deferred. +- **Files:** 36 touched by the plan's commits (3,285 + 1,133 + 589 insertions in the three code commits) + +## Accomplishments + +- **Task 1 (D-24):** the decision note `.planning/notes/2026-10-22-mcp-go-sdk.md` records the transitive graph, size, govulncheck result and build-tag containment. STACK.md gained a row for each module. The user approved both modules before `go get`. +- **Task 2 (tracer):** `modules/pier` contains the Host contract, the `mcp.*` config, the catalog compiled from `pact.HasMCP` and per-request identity through the published verifier and resolvers. It also has the built-in backend guard provider, in-process dispatch with `bouncer.WithDispatch`, a per-identity SDK server with a per-request re-filter, the stateful session router over a memory store and audit lines. `modules/surf/pier_on.go` sets the `mountPier` hook. `TestPierTracerSmoke` lists and calls `acme_echo` end to end. +- **Task 3:** `store_redis.go` stores records with PX TTLs, keeps per-binding sets and carries pub/sub events, under cooler's prefix and opened lazily. `events.go` republishes `pact.MCPSubjectChanged` through the store, so every instance sends list_changed or ends the subject's sessions. `stateless.go` serves 2026-07-28 clients. `prm.go` serves the metadata at both well-known paths, and long GET streams re-verify their bearer every minute. These are covered by `TestPierSessionsSmoke`, `TestPierFanOutSmoke` and `TestPierStatelessSmoke` on miniredis. +- **Task 4:** personal tokens are accepted only when `mcp.allow_personal_tokens` is on. An admin token gives a backend-only identity with its scope limit, and a user token gives a frontend-only one. The read/write ceiling, prompts and the audit line are asserted in the tracer's subtests. This task also added `docs/services/mcp.md` with the tested `acme.blog` example, the site.yaml entry and cross-links from the authentication, registration and surf pages. +- Verified again at close: `go test -count=1 -short ./modules/pier/` passes all five tests (`TestExampleBlogPlugin`, `TestPierSessionsSmoke`, `TestPierFanOutSmoke`, `TestPierStatelessSmoke`, `TestPierTracerSmoke`), and `go build -tags summercms_mcp ./...` succeeds. + +## Task Commits + +1. **Task 1: Decide the MCP Go SDK** - `3ea82a1` (docs) +2. **Task 2: pier tracer** - `cfdbaf0` (feat) +3. **Task 3: Redis sessions, events, stateless path, PRM** - `806e87c` (feat) +4. **Task 4: Identity kinds, ceilings, prompts, MCP services page, READMEs** - `8c6c0a3` (feat), plus `1aff96f` (docs: deferred items) +5. **Task 5: Release decision** - answered by the user, no commit (see below) +6. **Task 6: Release** - deferred, no commit + +**Commit count note:** `commits: 29` is measured as `git rev-list --count 3746675..a26ee2b`. That range also contains 24 commits from concurrent sessions (msd-quick tasks 261022-k9k, -tb4, -tto, -usi, -vfz, -wah, -x7a and their planning docs). This plan's own commits are the five listed above. + +## Release (Tasks 5 and 6): deferred + +The user's final answer to Task 5 replaces the earlier v0.5.0 answer: **no tag in this plan.** + +- The pier code ships **opt-in in framework v0.4.4**. To use it, an application is built with `-tags summercms_mcp` and sets `mcp.enabled: true` (the default is off). Without the tag the binary carries no go-sdk. The orchestrator handles v0.4.4 outside this plan, after the plan closes. +- **v0.5.0 is reserved for phase 19.7 completion** (plans 01-05 and phase verification, i.e. full MCP usability). +- This plan did not tag, push, post a changelog or edit the decision note's "Released in" line. The plan's D-31 truth and the Task 6 acceptance criteria (tag on origin, changelog URL) are therefore not met here. The release is tracked by the 19.7 decisions in STATE.md. +- The Docker-host gate from the plan's verification (`go test -count=1 ./...` and `bash scripts/check-phase19.6.1.sh --all` before the tag) has to run before the v0.4.4 tag. It was not run in this close. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] The tracer client is pinned to protocol 2025-11-25** +- **Found during:** Task 2 +- **Issue:** an SDK client left at its default negotiates the newest protocol, which pier sends to the stateless path. The tracer has to exercise the stateful path with sessions. +- **Fix:** `client.Connect(..., &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})` in `TestPierTracerSmoke`, and `connectPinned` for the identity subtests +- **Files modified:** modules/pier/tracer_smoke_test.go, modules/pier/identity_smoke_test.go +- **Committed in:** cfdbaf0, 8c6c0a3 + +**2. [Rule 1 - Bug] SDK client sessions are closed by defer before the test server closes** +- **Found during:** Task 2 +- **Issue:** if `httptest.Server.Close` runs while an SDK session's GET stream is still open, it blocks waiting on the hanging connection +- **Fix:** `defer cs.Close()` is registered after `defer srv.Close()`, so the session closes first (LIFO) +- **Files modified:** modules/pier/tracer_smoke_test.go +- **Committed in:** cfdbaf0 + +**3. [Rule 3 - Blocking] `TestExampleBlogPlugin` calls `blogTools()`** +- **Found during:** Task 4 +- **Issue:** the docs example has to be a compiled, tested declaration that `docs/services/mcp.md` can copy through `src=` +- **Fix:** the declaration lives in `blogTools()`. The example plugin's `MCP()` returns it, and the test calls it directly to assert 3 tools and 1 prompt before mounting the plugin. +- **Files modified:** modules/pier/example_mcp_test.go +- **Committed in:** 8c6c0a3 + +**4. [Rule 3 - Blocking] `*pier.Pier` is published on the app as a test seam** +- **Found during:** Task 4 +- **Issue:** the identity subtests need to inject a personal-token resolver and reach the mounted instance. The cabana resolver needs a database, which is not available under `-short`. +- **Fix:** `Mount` publishes the `*pier.Pier` on the application, and `export_test.go` exposes the unexported seams to tests only +- **Files modified:** modules/pier/pier.go, modules/pier/export_test.go +- **Committed in:** 8c6c0a3 + +--- + +**Total deviations:** 4 auto-fixed (1 bug, 3 blocking). The release (Task 6) was deferred by the user's decision. +**Impact on plan:** the code scope is unchanged. The release moved out of the plan. + +## Issues Encountered + +None beyond the deviations and the deferred items below. + +## Follow-ups (from deferred-items.md) + +- **open:** stateful pier sessions cannot carry the SDK's protocol-version context value. go-sdk v1.8.0 keeps it under an unexported key, so stateful streams run with the 2025-03-26 defaults for close/reconnect hints and JSON-RPC batching. Revisit if the SDK exports the key. Plan 05 may pin the behaviour in a test. +- **open:** `notifications/initialized` can land on a different instance than `initialize`. The first instance's local copy then refuses calls until it is dropped. Sticky sessions or a per-request re-read of the stored `initialized` flag would fix this. It does not affect a single-instance deployment. +- **open (from 19.7-01):** pre-existing `gofmt` drift in six cabana files. The fix is a separate `style(cabana)` commit. +- Before any framework tag: run `bash scripts/check-phase19.6.1.sh --all` and the full Docker `go test -count=1 ./...` on the Docker host. + +## Known Stubs + +None. The release is deferred by decision; it is not a stub. + +## User Setup Required + +None for this plan. Applications that want MCP build with `-tags summercms_mcp` and set `mcp.enabled: true` (see `docs/services/mcp.md`). + +## Next Phase Readiness + +- **Plan 03 (sm-user-plugin):** publish `pact.MCPLinkedBackendResolver` and `pact.MCPPersonalTokenResolver`, frontend permissions, and the link and unlink record actions. pier picks them up through `app.Lookup`. +- **Plan 04 (oauthserver):** publish `pact.MCPTokenVerifier` and add `backend` to `ScopesSupported`. The PRM `resource` must match the verifier's resource. +- **Plan 05:** unit tests and security review, including the two open pier items above and the subject-event listener assertions carried over from 01. +- **Framework version for plans 03 and 04:** v0.4.4 (pier opt-in), once the orchestrator tags it. v0.5.0 follows at phase completion. + +--- +*Phase: 19.7-generic-mcp-module* +*Completed: 2026-10-23* + +## Self-Check: PASSED + +All key created files exist on disk (pier sources, surf/pier_on.go, docs/services/mcp.md, the pier README, the decision note) and the five plan commits (3ea82a1, cfdbaf0, 806e87c, 8c6c0a3, 1aff96f) are in history. `go test -count=1 -short ./modules/pier/` and `go build -tags summercms_mcp ./...` pass at a26ee2b.