⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
src/mcp/discovery-routes.ts:7 states what the .well-known documents are for:
// One handler factory, two callers: the Worker mounts these on the cloud deployment, and the self-host app
// mounts the SAME routes over its own availability-filtered tool list. That is what makes a self-hosted
// card truthful rather than a copy of the cloud one — a `cloud`-only tool is absent from a self-host card
// because it is absent from that deployment's list ...
The filter it applies is availability only (src/mcp/discovery-routes.ts:41):
return listToolDefinitions({ availability: [deployment] });
availability is not the only thing that decides whether a deployment serves a tool. The five admin
tools are registered conditionally, on a flag that is off by default —
src/mcp/server.ts:786:
/** Master opt-in for the "admin" tool category (#7721), default OFF. ... Gates tool REGISTRATION in
* createServer() below ... */
function isMcpAdminEnabled(env: Env): boolean {
return /^(1|true|yes|on)$/i.test((env.LOOPOVER_MCP_ADMIN_ENABLED ?? "").trim());
}
All five (loopover_admin_get_config, loopover_admin_write_config,
loopover_admin_list_config_backups, loopover_admin_trigger_redeploy, loopover_admin_rotate_secret)
declare availability: "selfhost" — packages/loopover-contract/src/tools/admin-config.ts:56, :92,
:121, :160, :206. So on a self-hosted instance, toolsForDeployment("selfhost") returns them
unconditionally, and all four discovery documents list them:
/.well-known/mcp.json — name, title, description, category, annotations (buildServerCard,
packages/loopover-contract/src/discovery.ts:107)
/.well-known/agent-tools/index.json — the above plus both full JSON Schemas
/.well-known/agent-tools/openai.json and .../anthropic.json — name, description, parameters
The routes are unauthenticated by design (src/auth/route-auth.ts:81 exempts them from the token check;
src/openapi/discovery-route-specs.ts:58 declares auth: "public"). The result on a default self-host
deployment: an anonymous reader is handed a machine-readable tool catalog — including a
destructiveHint: true entry (admin-config.ts:205) — for five tools that /mcp does not register and
will refuse as unknown. test/contract/validate-mcp.test.ts:210 records the same fact from the other
side: the admin tools "register only when LOOPOVER_MCP_ADMIN_ENABLED is set, and every other case here
boots a server without it".
That is exactly the untruthful card the module header says these routes exist to prevent, and it is worse
than a cloud-only tool leaking onto a self-host card: an agent following
/.well-known/agent-tools/openai.json will emit a tool call the server cannot answer.
Requirements
toolsForDeployment must take the same registration condition into account as createServer, so the
four discovery documents describe only tools the answering deployment actually registers.
- The condition must be read from
c.env at request time, alongside the existing
isSelfHostedReviewRuntime(c.env) deployment read at src/api/routes.ts:645 — not captured at module
load, and not duplicated as a second copy of the truthy-string regex. Reuse the existing predicate that
src/mcp/server.ts:789 implements (export it if it is not already exported).
- The memo key in
discoveryDocumentsFor (src/mcp/discovery-routes.ts:103) must include the new axis, or
the first request on an isolate permanently fixes the wrong document for every later one.
- Only the
admin category is gated by a registration flag today. The fix must be expressed in terms of
"tools this deployment registers", not a hardcoded category !== "admin" filter buried in the route
handler.
- What must NOT change: the cloud deployment's documents (the admin tools are
availability: "selfhost",
so they were never on the cloud card), the ETag/304 behaviour in respondWithDocument
(src/mcp/discovery-routes.ts:79), the deterministicGeneratedAt derivation, and the public/no-auth
posture of the four routes.
- What must NOT change: the admin tools' registration gate itself, their
auth: "mcp-admin" declarations,
or any per-tool authorization. This issue is about what the catalog advertises, nothing else.
⚠️ Required pattern: the deployment read at src/api/routes.ts:645 — a request-time c.env predicate
feeding discoveryDocumentsFor's context, with the value participating in the cache key at
src/mcp/discovery-routes.ts:103. What does NOT satisfy this issue: (a) filtering inside
packages/loopover-contract/src/discovery.ts, which is the pure, env-free projection layer and must stay
that way ("Availability filtering is the CALLER's job", discovery.ts:10); (b) dropping the admin
category from the registry so it disappears from every surface including the enabled one; (c) leaving the
memo key unchanged, which makes the fix depend on which request an isolate happens to serve first.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example filtering the tool list without touching the cache key, or adding only the flag-off test — does
not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's
coverage.include covers src/**/*.ts (line 78), so src/mcp/discovery-routes.ts, src/api/routes.ts
and src/mcp/server.ts are all measured and gated. Both arms of the new admin-enabled branch need a
test, and the existing isSelfHostedReviewRuntime(c.env) ? "selfhost" : "cloud" ternary at
src/api/routes.ts:645 must keep both of its arms covered — the cloud arm now interacts with the new
filter and must be asserted to be unaffected.
Expected Outcome
A default self-hosted LoopOver's public tool catalog lists only the tools its /mcp endpoint actually
registers, so an agent that reads /.well-known/agent-tools/*.json never emits a call the server will
reject as unknown — and the "truthful rather than a copy" property the module header claims becomes true
for the one category whose registration is conditional.
Links & Resources
src/mcp/discovery-routes.ts:7 — the truthfulness claim
src/mcp/discovery-routes.ts:38 — toolsForDeployment, filtering on availability alone
src/mcp/discovery-routes.ts:100 — the memo and its key
src/mcp/server.ts:786 — isMcpAdminEnabled, the registration gate
packages/loopover-contract/src/tools/admin-config.ts:56 — the five availability: "selfhost" admin entries
src/auth/route-auth.ts:81 — the discovery routes are unauthenticated
test/contract/validate-mcp.test.ts:210 — the validator's note that admin tools are unregistered by default
Context
src/mcp/discovery-routes.ts:7states what the.well-knowndocuments are for:The filter it applies is
availabilityonly (src/mcp/discovery-routes.ts:41):availabilityis not the only thing that decides whether a deployment serves a tool. The fiveadmintools are registered conditionally, on a flag that is off by default —
src/mcp/server.ts:786:All five (
loopover_admin_get_config,loopover_admin_write_config,loopover_admin_list_config_backups,loopover_admin_trigger_redeploy,loopover_admin_rotate_secret)declare
availability: "selfhost"—packages/loopover-contract/src/tools/admin-config.ts:56,:92,:121,:160,:206. So on a self-hosted instance,toolsForDeployment("selfhost")returns themunconditionally, and all four discovery documents list them:
/.well-known/mcp.json— name, title, description, category, annotations (buildServerCard,packages/loopover-contract/src/discovery.ts:107)/.well-known/agent-tools/index.json— the above plus both full JSON Schemas/.well-known/agent-tools/openai.jsonand.../anthropic.json— name, description, parametersThe routes are unauthenticated by design (
src/auth/route-auth.ts:81exempts them from the token check;src/openapi/discovery-route-specs.ts:58declaresauth: "public"). The result on a default self-hostdeployment: an anonymous reader is handed a machine-readable tool catalog — including a
destructiveHint: trueentry (admin-config.ts:205) — for five tools that/mcpdoes not register andwill refuse as unknown.
test/contract/validate-mcp.test.ts:210records the same fact from the otherside: the admin tools "register only when LOOPOVER_MCP_ADMIN_ENABLED is set, and every other case here
boots a server without it".
That is exactly the untruthful card the module header says these routes exist to prevent, and it is worse
than a
cloud-only tool leaking onto a self-host card: an agent following/.well-known/agent-tools/openai.jsonwill emit a tool call the server cannot answer.Requirements
toolsForDeploymentmust take the same registration condition into account ascreateServer, so thefour discovery documents describe only tools the answering deployment actually registers.
c.envat request time, alongside the existingisSelfHostedReviewRuntime(c.env)deployment read atsrc/api/routes.ts:645— not captured at moduleload, and not duplicated as a second copy of the truthy-string regex. Reuse the existing predicate that
src/mcp/server.ts:789implements (export it if it is not already exported).discoveryDocumentsFor(src/mcp/discovery-routes.ts:103) must include the new axis, orthe first request on an isolate permanently fixes the wrong document for every later one.
admincategory is gated by a registration flag today. The fix must be expressed in terms of"tools this deployment registers", not a hardcoded
category !== "admin"filter buried in the routehandler.
availability: "selfhost",so they were never on the cloud card), the ETag/304 behaviour in
respondWithDocument(
src/mcp/discovery-routes.ts:79), thedeterministicGeneratedAtderivation, and the public/no-authposture of the four routes.
auth: "mcp-admin"declarations,or any per-tool authorization. This issue is about what the catalog advertises, nothing else.
Deliverables
toolsForDeployment(or its caller insrc/api/routes.ts:639-653) excludes tools whose category isregistered only under
LOOPOVER_MCP_ADMIN_ENABLEDwhen that flag is not set, using the samepredicate
createServeruses.discoveryDocumentsFor's cache key includes the new axis.LOOPOVER_MCP_ADMIN_ENABLEDunset and aself-host env,
GET /.well-known/mcp.jsonandGET /.well-known/agent-tools/index.jsonlist none ofthe five admin tool names.
LOOPOVER_MCP_ADMIN_ENABLED=1on the same self-host env, allfive admin tool names ARE listed in both documents.
the flag-on document from the same module instance returns two different bodies with two different
ETags (
resetDiscoveryCacheForTestingatsrc/mcp/discovery-routes.ts:121must not be needed to makethis pass).
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example filtering the tool list without touching the cache key, or adding only the flag-off test — does
not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includecoverssrc/**/*.ts(line 78), sosrc/mcp/discovery-routes.ts,src/api/routes.tsand
src/mcp/server.tsare all measured and gated. Both arms of the new admin-enabled branch need atest, and the existing
isSelfHostedReviewRuntime(c.env) ? "selfhost" : "cloud"ternary atsrc/api/routes.ts:645must keep both of its arms covered — the cloud arm now interacts with the newfilter and must be asserted to be unaffected.
Expected Outcome
A default self-hosted LoopOver's public tool catalog lists only the tools its
/mcpendpoint actuallyregisters, so an agent that reads
/.well-known/agent-tools/*.jsonnever emits a call the server willreject as unknown — and the "truthful rather than a copy" property the module header claims becomes true
for the one category whose registration is conditional.
Links & Resources
src/mcp/discovery-routes.ts:7— the truthfulness claimsrc/mcp/discovery-routes.ts:38—toolsForDeployment, filtering onavailabilityalonesrc/mcp/discovery-routes.ts:100— the memo and its keysrc/mcp/server.ts:786—isMcpAdminEnabled, the registration gatepackages/loopover-contract/src/tools/admin-config.ts:56— the fiveavailability: "selfhost"admin entriessrc/auth/route-auth.ts:81— the discovery routes are unauthenticatedtest/contract/validate-mcp.test.ts:210— the validator's note that admin tools are unregistered by default