Skip to content

mcp(discovery): exclude admin tools from a self-host card when the admin surface is not enabled #10039

Description

@JSONbored

⚠️ 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

  • toolsForDeployment (or its caller in src/api/routes.ts:639-653) excludes tools whose category is
    registered only under LOOPOVER_MCP_ADMIN_ENABLED when that flag is not set, using the same
    predicate createServer uses.
  • discoveryDocumentsFor's cache key includes the new axis.
  • A regression test named for this bug asserting that, with LOOPOVER_MCP_ADMIN_ENABLED unset and a
    self-host env, GET /.well-known/mcp.json and GET /.well-known/agent-tools/index.json list none of
    the five admin tool names.
  • A test asserting the positive arm: with LOOPOVER_MCP_ADMIN_ENABLED=1 on the same self-host env, all
    five admin tool names ARE listed in both documents.
  • A test asserting the memo does not leak between the two: requesting the flag-off document and then
    the flag-on document from the same module instance returns two different bodies with two different
    ETags (resetDiscoveryCacheForTesting at src/mcp/discovery-routes.ts:121 must not be needed to make
    this 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'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:38toolsForDeployment, filtering on availability alone
  • src/mcp/discovery-routes.ts:100 — the memo and its key
  • src/mcp/server.ts:786isMcpAdminEnabled, 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions