Every role-local MCP spec gates itself on the service flag and skips when the surface is off, so nothing ever checked that switching it off removes it. A surface still serving after the flag went false was indistinguishable from one that was never deployed: both read as a green skip. The gating helper had no inverse at all, so there was no way to assert what a disabled service must not do.
A shared assertion now probes the endpoint with both a GET and a POST while the flag is false and fails on any answer that shows something listening. It counts 401 and 403 as listening, which is the point: a refused credential still means the surface is there. The check is transport-agnostic because the roles differ in their enabled-state probes, over SSE, Streamable HTTP or plain JSON-RPC, while an endpoint that is gone answers none of them. Each role passes its endpoint URL as a thunk, since some resolve it only once a test is running.
The coverage lint requires the registration, and derives the role set from meta/mcp.yml rather than from a filename convention, which is how the tenth role surfaced: its spec is not named like the other nine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Provision the native MCP server surface: create the endpoint, its owner
and its workspace, probe it on the internal origin, and refuse an
endpoint that never serves the stream. Baserow addresses a session
through the URL path, so meta/services.yml declares the endpoint key and
suffix the discovery layer composes the client URL from.
Split the role into a run-once guarded core. Registering the MCP surface
makes baserow a shared provider, and the variant-aware loader checks the
run-once flag before loading; without the flag as the first task a second
load request can include the role again.
Address the container by its hosting node. Migration and bootstrap ran
`container exec` on the manager while the replica sits on a worker, so
the resolved id existed nowhere the command could reach it. Re-resolve
after the post-migration restart as well: `service update --force`
replaces the container, and the bootstrap was still holding the id of the
one it replaced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>