Commit Graph
3 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 c94a0a0b5d test(mcp): ask the ten enabled surfaces the hostile question
Nine roles enabled an MCP surface without a browser-side spec, so nothing
ever asked what the endpoint answers a caller with no credential at all.
Lift gitea's assertion into two shared registrars and register them per
role, and give each role the MCP_SERVICE_ENABLED flag they gate on.

mcpEndpointUrl now takes the base URL rather than reading APP_BASE_URL
unconditionally: jellyfin and zammad export their own base variable, and
declaring APP_BASE_URL for them would un-skip the shared guest persona,
which collapses itself on that variable being absent.

The env-gate scanner follows require("./x") into the shared harness so a
gate declared there counts as consumed, and strips block comments first
so service-gating.js's own doc examples do not credit EMAIL, SSO and OICD
to every role that imports it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:06:19 +02:00
kevinveenbirkenbachandClaude Opus 5 7d8e7e87ff test(mcp): prove a switched-off surface is actually gone
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>
2026-08-29 00:40:15 +02:00
kevinveenbirkenbachandClaude Opus 5 3036b7ba19 feat(gitea): serve MCP through the project-owned sidecar
Gitea ships no MCP server, so the role runs the project-owned gitea-mcp
package as a sidecar next to it and points it at the instance over the
container network.

Two things the image forces. Its command carries the binary path
explicitly because the image declares no ENTRYPOINT, so a bare flag list
would replace the binary instead of extending it. The port is stringified
because Ansible's native types make a bare lookup an int, and compose
rejects a non-string in a command array.

Keep the surface internal. The public vhost answers a bearerless probe
with an SSO redirect, so the guest spec pins maxRedirects to 0 and
asserts no MCP response is ever served; following the redirect would land
on a login page and pass for the wrong reason.

Calls run with administrator rights, so the tool policy is pinned
read-only and the sidecar starts with the read-only flag and a scope
list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:10:15 +02:00