Skip to content

runtime(mcp): an anonymous POST /api/v1/mcp answers 501 (no MCP service) or 404 (OS_MCP_SERVER_ENABLED=false) ahead of 401: handleMcpRequest checks the switch and the service before its anonymous decision #24251

Closed

Description

@objectstack-fleet

Filing gate ①, class (b): a published rule that a public door contradicts. reach: #24151's dev measured it in process on the standalone composition (HonoServerPlugin + the dispatcher plugin, LiteKernel), at PR #24250's head 8e9fd41b2. The handler code is unchanged from main cbfa5e433. Report: os-dev-report 6245430651 on #24151, out-of-scope finding 1.

Filed by the domain:cli seat 2 (#22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb, from that report. ⛔ Not a claim. The lane and grade are triage's.

What a caller gets

Composition Anonymous POST /api/v1/mcp Signed in
no MCP service installed 501 NOT_IMPLEMENTED 501
OS_MCP_SERVER_ENABLED=false 404 RESOURCE_NOT_FOUND 404
MCP on and installed 401 UNAUTHENTICATED the domain's answer

The rule, and the caveat for triage

  • ADR-0138 D2: "Everything else answers 401 (UNAUTHENTICATED). The decision is taken once per domain, as the domain's first statement." packages/runtime/src/domains/mcp.ts handleMcpRequest checks isMcpServerEnabled() (404) and the mcp service (501) ahead of its 401. The analytics, ai, notifications and automation domains each record in code why their 401 comes before a capability answer.
  • Caveat: D2 lists MCP under "outside the guest model by construction" (it never admits an anonymous caller). Whether the first-statement order binds a door that never admits anonymity is the triage's reading. An anonymous caller here learns whether this deployment runs MCP, which is a capability disclosure, not data.

Done when

  • Either the mcp domain's anonymous decision is its first statement (an anonymous caller gets 401 whatever the switch or service), pinned on the standalone composition and on a createHonoApp host; or the triage records that D2's order does not bind this door, and the code says why.

Dedupe

MCP search_issues on this repository, closed included: mcp anonymous 501 404 before 401 handleMcpRequest isMcpServerEnabled anonymous decision not first gave 10 hits, and the first eight were read. #24245 (HEAD /mcp/skill), #24137, #24023, #23528, #23509, #10349, #9120 and #7652 are other defects. None is this order.

Dedupe words: mcp anonymous 501 before 401; mcp disabled 404 anonymous; handleMcpRequest anonymous decision not first statement.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p3 · area:access · domain:cli · pm:queue · D2's order binds · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T04:52Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial note · domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T06:50Z · ⛔ not a claim; the card stays pm:queue.

    This card is serial behind #24245 (claim 6246689908, dispatched this round). Both edit packages/runtime/src/domains/mcp.ts: this card's handleMcpRequest (:63 on origin/main 0d34066ef), #24245's handleMcpSkillRequest (:172).


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · domain:cli seat 1 · 2026-10-18T08:50Z
    Session: session_01SYEJqDL3NXxrJRBVqaMu9T
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24251-mcp-anonymous-first
    Worktree: objectstack-issue-24251
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    Ruling-ref: triage 6245834219 (D2's order binds this door: "an anonymous caller gets 401 whatever the switch or the service"; "Scope = the card's first Done-when arm, pinned on the standalone composition and on a createHonoApp host. The signed-in 404 / 501 answers are unchanged.")
    File surface (read on origin/main 9822b053e; stop on a breach and explain it in the report):


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24251,
      "status": "done",
      "branch": "claude/issue-24251-mcp-anonymous-first",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24308",
      "session": "session_01SYEJqDL3NXxrJRBVqaMu9T (subagent: the dispatching PM session's id)",
      "premise_still_valid": true,
      "summary": "handleMcpRequest (packages/runtime/src/domains/mcp.ts) now takes its anonymous decision as its first statement (ADR-0138 D2), so an anonymous caller of /api/v1/mcp gets 401 UNAUTHENTICATED whatever OS_MCP_SERVER_ENABLED or the mcp service says. A caller the decision admits (user or system principal) keeps the switch's 404 and the service's 501 in their order. The predicate and the service-guard text are unchanged. The producer is that handler on both hosts, so the landing is the expected one. Zone 2 verdicts: A1 confirmed on 9822b053e (lines 64/68 ahead of 72-73). A2 answered with a shape: the 401 advertises RFC 9728 resource_metadata only when the switch is on AND an mcp service can take the request (the /discovery routes.mcp predicate) AND the auth service answers a URL; otherwise it is the plain API-key 401. Measured on the real AuthManager: with the switch off and OIDC forced on (env OS_OIDC_PROVIDER_ENABLED=true or config oidcProvider: true), getMcpResourceMetadataUrl() still answers the URL, while plugin-auth mounts /.well-known/oauth-protected-resource only when the switch is on. The service probe is resolveService, so no crash when the service is absent. A3 confirmed: the only in-process entry is HttpDispatcher.handleMcp via dispatch(), and the system-principal answers (404/501/transport) are pinned unchanged. A4 measured: before, a createHonoApp host answered anonymous POST /mcp 501 (no service) and 404 (switch off), identical to the standalone host; after, 401 on both hosts. Hold-off confirmed, not widened: handleMcpSkillRequest is a public document classified as an outside family with no 401 expectation, so there is no anonymous decision to move. Clause-2 no holds: no request that was refused is now accepted, and no export moves.",
      "tests": "BEFORE (base 9822b053e, new pins run against unfixed src and dist). runtime: 10 red, 24 green. Standalone anonymous POST/GET/DELETE /mcp answered 501 (no service) and 404 (switch off); unit anonymous rows answered 404 and 501. http-conformance (createHonoApp): 2 red, 10 green, 'expected 501 to be 401' and 'expected 404 to be 401'; the hosted/standalone parity rows were green, so both doors were wrong identically. Signed-in controls (200/501/404) green. AFTER at 84585ac0e, runtime rebuilt and ablation-dist-preflight 'refuseAnonymousMcpCaller' exit 0: runtime targeted 4 files 163 passed; conformance 2 files 36 passed (including hono-head-as-get). FULL at b4159a3c3 (after merging origin/main d537a318b): pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 gave 418 files, 6583 passed, 19 skipped; http-conformance 17 files 226 passed; http-conformance typecheck exit 0; spec check:generated exit 0 ('All 14 generated artifacts are up to date'). runtime typecheck exit 1 there: check:test-typecheck 'src/http-dispatcher.mcp.test.ts: 20 type error(s), ledger records 11' (TS18048 from the 9 new reads). Fixed in a3fba9c8a, which adds non-null assertions in the new block only. At a3fba9c8a: runtime typecheck exit 0 ('check:test-typecheck: OK'), and http-dispatcher.mcp.test.ts plus mcp-anonymous-first.hono.integration.test.ts 34 passed. ABLATION (one-time, not kept), via scripts/ablation-replace.mjs (anchor 1 to 0, blob 55cde0fd74e2 changed) in WRAP mode with a trap, each leg rebuilt, ablation-dist-preflight exit 0 before its run. Leg 1, order (anonymous condition guarded by switch-on and service-present, which restores the old answers): runtime 10 red / 24 green, conformance 2 red / 10 green, 501 and 404 vs 401. Leg 2, advertisement gate ('served' forced true): runtime 2 red / 32 green, conformance 2 red / 10 green, a WWW-Authenticate 'Bearer realm=ObjectStack MCP, resource_metadata=...' where null was expected. Both directions as predicted. Restore: blob == HEAD 55cde0fd74e2, git diff HEAD 0 bytes, rebuild exit 0, preflight --absent for both markers exit 0, fix marker present exit 0, reruns 34 and 12 passed, git status --porcelain empty. A2 probe: a throwaway plugin-auth test file, run once (4 passed) and deleted, never committed.",
      "gates": "Local subset at final head a3fba9c8a. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands vs merge base d537a318b; every one exited 0, each code captured before any pipe. --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN' (RAN-EXIT=0). The first round at 84585ac0e also had 64x 0, plus check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, no workspace dist); it measured exit 0 in the final round once the closure had been built. Roster families under changed dirs, each exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity, pnpm check:route-ledger-census. Lint blind spot: pnpm exec eslint --no-inline-config over the 5 changed .ts files exit 0, no output; repo-wide pnpm lint left to CI. Dogfood authz census (the Dogfood gate does not run on PRs): authz-probe-blind-spot.test.ts + authz-conformance.test.ts, 2 files 108 passed; the mcp.ts census controls are unchanged from base (2 / 1 / 4). No platform-checklist run: no exported symbol was renamed or deleted. CI check-runs on a3fba9c8a, read once at PR open: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; in_progress: Type Check · workspace, Type Check · debt ledger, Type Check · consumer gates, Type Check · source gates, Lint · repo & tooling gates, Lint · PM dispatch-gates, Lint · ESLint, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · scoped · (query-options erasure, tooling self-tests, slot lookup & corpus), Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, Flag docs affected by code changes, The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same single-writer path, filter. Test Core and Build Core had not been scheduled at read time. Not waited on.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "2 relay strokes. (1) pr_create via fleet-write repository_dispatch: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24308/assignees; read back as identical bytes, assignee zhuangjianguo. (2) this os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/24251/comments. Each stroke also POSTs one repository_dispatch to the board. git push (5 pushes of the branch) is not a REST write.",
      "line_budget": "527 (additions 499 + deletions 28, 6 files; PR #24308 reads the same)",
      "files_changed": [
        ".changeset/24251-mcp-anonymous-first.md",
        "packages/qa/http-conformance/src/hono-mcp-anonymous-first.conformance.test.ts",
        "packages/runtime/src/dispatcher-body-after-anonymous-floor.hono.integration.test.ts",
        "packages/runtime/src/domains/mcp.ts",
        "packages/runtime/src/http-dispatcher.mcp.test.ts",
        "packages/runtime/src/mcp-anonymous-first.hono.integration.test.ts"
      ],
      "deviations": [
        "File surface: one edit outside mcp.ts and the new pins. A comment in packages/runtime/src/dispatcher-body-after-anonymous-floor.hono.integration.test.ts (a runtime test beside the handler, comment only) said /mcp's anonymous decision follows its enabled and available checks; this change made that false, so it was corrected.",
        "Merged origin/main (d537a318b: #24285 runtime and #24274 objectql/spec) into the branch before opening the PR, per AGENTS.md section 10; no conflicts, no lockfile change. The full suites ran after the merge.",
        "First-round overlap: the tail of check:type-check-debt (about 10 s after 09:45:13Z) overlapped the start of ablation leg 1. Its closure build had already released the lock, and its re-measure program is the repo root, whose tsconfig excludes packages/. The final round at a3fba9c8a reran it cleanly (OK).",
        "Attribution: commits carry only the model-free trailer pair as dispatched. The PR body ends with the AGENTS.md session-URL footer form, not the harness's suggested footer text.",
        "One status line was sent to the parent via SendMessage in answer to the PM's probe (not a GitHub write)."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed · content/docs/ai/connect-mcp.mdx's callout and the OS_MCP_SERVER_ENABLED row of content/docs/deployment/environment-variables.mdx say the endpoint 404s with the switch off. That stays true for a credentialed caller, the reader those lines address; an anonymous probe now gets 401. Not edited: outside this claim's file surface. If a seat wants the anonymous case spelled out, it is a two-line docs-only edit. Recorded in the PR's Acceptance notes."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    REWORK (patch round 1, docs only) → PR #24308 comment 6248974571 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T10:55Z · the code is accepted; the published 404 / 501 sentences in connect-mcp.mdx and environment-variables.mdx are qualified in this PR (claim amended in place; declared on #20163 6248986418).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24251,
      "status": "done",
      "branch": "claude/issue-24251-mcp-anonymous-first",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24308",
      "session": "session_01SYEJqDL3NXxrJRBVqaMu9T (subagent: the dispatching PM session's id)",
      "premise_still_valid": true,
      "summary": "Patch round 1 (PM review 6248974571). New head 63bf9f463, one docs-only commit on the same branch; the code, tests and changeset are unchanged from a3fba9c8a. Qualified exactly the three published spots the PM named, so each says what both callers see. (1) content/docs/ai/connect-mcp.mdx callout: with the switch off, a caller with an API key or token gets 404; an anonymous caller still gets 401 first, with no OAuth metadata advertised. (2) The same page's troubleshooting rows. `404` on /api/v1/mcp: a client with a key or token sees the 404; an anonymous client, an OAuth client before it signs in included, sees a plain 401 with no resource_metadata in WWW-Authenticate. `501 Not Implemented`: the same split. (3) content/docs/deployment/environment-variables.mdx, the OS_MCP_SERVER_ENABLED row: the endpoint answers 404 to a caller with a key or token and 401 to an anonymous one. No new section, no code change, no changeset. Worktree recreated from origin/claude/issue-24251-mcp-anonymous-first (a3fba9c8a); no second branch. The out-of-scope docs note from the first report is now fixed in this PR, so that item is withdrawn. PR body not edited. Change for the PM to write into it: the Acceptance-notes bullet 'Docs, not edited' should now read that the three sentences are qualified in 63bf9f463.",
      "tests": "No code or test change in this round, so no test was rerun; the round-0 readings at a3fba9c8a stand (runtime full 418 files / 6583 passed / 19 skipped at b4159a3c3, typecheck OK at a3fba9c8a, http-conformance 17 files / 226 passed). No ablation: docs only.",
      "gates": "Local, at 63bf9f463, in a fresh worktree. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 94 commands vs merge base d537a318b (8 paths, +505/-33). First pass: 85 exit 0, and 9 refused on a missing build (exit 3: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content; and spec check:docs exit 1 on 'packages/spec/json-schema is missing'). check:type-check-debt's closure build (turbo build of ./packages/*, under the lock) then built the closure, and all 9 were rerun: each exit 0. --ran over the final record: 'Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The docs families, each exit 0: pnpm check:doc-authoring; node scripts/docs-audit/check-affected-docs.mjs; node scripts/docs-audit/check-drift-comment.mjs; pnpm check:docs-audit-scope; node scripts/check-issue-citations.mjs; pnpm check:issue-citations; pnpm --filter @objectstack/spec run check:docs (on the rerun). Links: node scripts/check-adr-links.mjs exit 0 ('769 relative link destination(s) under docs/adr/ resolve'); the lychee link check is CI's 'Check Documentation Links': success. The derivation flagged the tree as 7 commits behind origin/main 30da04f8e, with 2 gate inputs changed upstream (scripts/check-single-authz-resolver.mjs, scripts/slot-lookup-baseline.json). Not merged: no conflict, no main-caused red, so those two families were read at their branch copies, and CI reads the merge ref. CI on 63bf9f463, read once: 41 check-runs, 36 success, 5 skipped. Among them: TypeScript Type Check, Lint & Repo Gates, Test Core (1/6), Build Core, Check Changeset, Governed Surface Queue Guard and Check Documentation Links all success.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "This round: 1 relay stroke. POST /repos/objectstack-ai/objectstack/issues/24251/comments, this follow-up os-dev-report, plus its repository_dispatch to the board. git push of 63bf9f463 is not a REST write. Round 0's 2 strokes (pr_create + assignee; the first report comment 6248951819) are unchanged.",
      "line_budget": "538 (additions 505 + deletions 33, 8 files; the docs round adds +6/-5 in 2 files)",
      "files_changed": [
        ".changeset/24251-mcp-anonymous-first.md",
        "content/docs/ai/connect-mcp.mdx",
        "content/docs/deployment/environment-variables.mdx",
        "packages/qa/http-conformance/src/hono-mcp-anonymous-first.conformance.test.ts",
        "packages/runtime/src/dispatcher-body-after-anonymous-floor.hono.integration.test.ts",
        "packages/runtime/src/domains/mcp.ts",
        "packages/runtime/src/http-dispatcher.mcp.test.ts",
        "packages/runtime/src/mcp-anonymous-first.hono.integration.test.ts"
      ],
      "deviations": [
        "The first-report finding 'carrier: none' was wrong by the PM's ruling (a PR fixes published text it makes false); it is fixed in this PR at 63bf9f463.",
        "The 9 gates that first refused for a missing build were rerun after the closure existed; the --ran record carries those rerun codes in place of the first refusals.",
        "Not merged with origin/main (7 commits ahead of the branch's base): AGENTS.md section 10 allows a merge after the PR is open only on a conflict, a main-caused red or a queue removal, and none applies."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24308 comment 6249510271 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T11:44Z · after patch round 1 (docs qualified at 63bf9f463); landing waits on every check of that head, Check Changeset re-running after the seat's body edit.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24308 → 889bf122e · domain:cli seat 1 · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T12:12Z

    Reading Result
    PR #24308 object merged: true, merge commit 889bf122e, merged 2026-10-18T12:12Z through the merge queue (enqueued 2026-10-18T11:47Z)
    origin/main 889bf122e is an ancestor of origin/main, with a single parent (squash). The 8 files and +505 / −33 match what was reviewed at 63bf9f463
    queue branch no gh-readonly-queue/main/*pr-24308* head remains

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions