Skip to content

runtime(mcp): on a createHonoApp host, HEAD /api/v1/mcp/skill answers 405 with Allow: GET while GET is served — the catch-all hands dispatch() the raw HEAD and handleMcpSkillRequest refuses every non-GET #24245

Closed

Description

@objectstack-fleet

Filing gate ①, class (a): a measured defect at a public door. reach: #24228's dev measured it in process, through createHonoApp's own HTTP pipeline (app.request, the host's Hono app with its catch-all), on PR #24244's tree (os-dev-report 6244534422 on #24228, out-of-scope finding 2). A real socket on a createHonoApp host is NOT MEASURED. On the standalone server, HEAD is answered from the GET route, so the defect is only on the createHonoApp host.

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

  • GET /api/v1/mcp/skill on a createHonoApp host: served.
  • HEAD /api/v1/mcp/skill on the same host: 405 METHOD_NOT_ALLOWED + Allow: GET.

RFC 9110 §9.3.2: a server that serves GET on a resource serves HEAD on it (the same answer, no content). The Allow: GET the 405 itself carries names the verb whose HEAD it just refused.

Why (as measured)

  • createHonoApp's catch-all hands dispatch() the raw request method, HEAD. The standalone server answers HEAD from the GET route instead.
  • packages/runtime/src/domains/mcp.ts handleMcpSkillRequest refuses every method that is not GET.

Done when

Dedupe

MCP search_issues on this repository, closed included: HEAD mcp skill 405 Allow GET createHonoApp HEAD method refused gave 4 hits.

None is HEAD.

Dedupe words: HEAD mcp skill 405; createHonoApp HEAD domain non-GET; mcp skill HEAD Allow GET.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p3 · area:api · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T02:59Z

    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 2 (seat post #22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T03:45Z · ⛔ not a claim; the card stays pm:queue.

    This card is serial behind #24151 (claim 6244946396, in flight). Both may touch the same request path into the mcp domain.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · domain:cli seat 1 · 2026-10-18T06:43Z
    Session: session_01SYEJqDL3NXxrJRBVqaMu9T
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24245-mcp-skill-head
    Worktree: objectstack-issue-24245
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main 0d34066ef; 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": 24245,
      "status": "done",
      "branch": "claude/issue-24245-mcp-skill-head",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24286",
      "session": "session_01SYEJqDL3NXxrJRBVqaMu9T",
      "premise_still_valid": true,
      "summary": "The fix lives at createHonoApp's catch-all in packages/adapters/hono/src/index.ts, chosen by measurement. A module-private dispatchedMethod() hands dispatch() GET for a HEAD and leaves every other verb as sent. The request beside it is still the client's raw HEAD. Hono drops the body itself (A4).\nMeasured base 53ef75243, in process. Hosted door: createHonoApp through app.request, with dispatch() wrapped. Standalone door: HonoServerPlugin socket plus createDispatcherPlugin. Grid: 34 paths x GET/HEAD x anonymous/signed in.\nA1-A3 confirmed. On the hosted door, 13 of 68 HEAD rows differed from their GET, across 7 route families. The standalone door matched GET on every row. At head, every hosted HEAD equals its GET, and no GET row moved.\nThe shape is the dispatcher's verb-equals-GET decisions (domain methods filters, m === 'GET' branches, the discovery root, the share-link public-GET floor), not the mcp domain. So a domain-level fix would have closed one family out of seven.\nTwo more edits. (1) Under the in-place exemption, the auth arm's /auth/config branch now answers HEAD as its GET (it was 404). (2) packages/runtime/src/domains/approvals.ts now reads HEAD off the forwarded request's own method. This is required: with the catch-all change alone, hosted HEAD /approvals/act answered 405 Allow: GET, POST, and hono-approvals-act.conformance's HEAD pin went red.\nOther GET-serving domains with the same shape on a createHonoApp host. All are closed by this change, because all reach dispatch() through the one catch-all.\nMeasured, GET vs HEAD at base:\n- /health and /ready (methods ['GET']): 200 vs 404 ROUTE_NOT_FOUND\n- the scoped discovery root /environments/:id: 200 vs 404\n- /analytics/meta, signed in: 200 vs 404\n- /ai/agents, signed in: 200 vs 501\n- data reads, signed in: 503 vs 404\n- share-links public resolve, anonymous: 501 vs 401\n- the auth arm's /auth/config, with an auth service: 200 vs 404\nCode-read, not measured: their service-absence check precedes the verb check in the measuring boot. These are i18n, automation GET faces, notifications, packages GET faces, security suggested-bindings, the other share-links GET faces, and meta's !method || method === 'GET' reads.\nPM checks: on a path that serves no GET (/keys, /actions), HEAD keeps 405 + Allow: POST signed in and 401 anonymous, unchanged on both doors. Every anonymous HEAD now gets the anonymous GET answer. The /mcp transport is handed HEAD on both doors.",
      "tests": "Commands and results:\n- New packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts: 4 cases, green.\n- New packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts: 24 cases, real HttpDispatcher, createHonoApp app.request beside the standalone socket, green.\n- New case in packages/runtime/src/domains/approvals-act.test.ts, green.\n- PR #24244's standalone pins, plus approvals-act and http-dispatcher.mcp: 7 files, 291 passed. The pins are mcp-skill-method-not-allowed, actions-every-verb, keys-every-verb, notifications-every-verb and dispatcher-body-after-anonymous-floor .hono.integration.\n- Controls in the same run: hono-approvals-act, hono-webhooks-redeliver and hono-meta-item-read-gate conformance, 4 files, 42 passed.\nPackage runs, all through os-verify-lock, VERDICT command-exit 0:\n- @objectstack/hono test: 10 files, 211 passed. typecheck: exit 0. Both at 379b734a2.\n- @objectstack/http-conformance test: 16 files, 214 passed. typecheck: exit 0. Both at 379b734a2. The first typecheck at 0a5693634 failed with TS2322 in the new test (app.request returns Response | Promise of Response); fixed in 379b734a2.\n- @objectstack/runtime test (local project): 416 files, 6548 passed and 19 skipped. typecheck: exit 0. Both at 0a5693634; no runtime file changed after it, and the merge at 4f14775b4 brought scripts/pm/dispatch-gates* only.\nAblations (fix committed first; scripts/ablation-replace.mjs wrap mode; restore proven blob == HEAD and git diff HEAD empty):\n1. dispatchedMethod body 'return method === HEAD ? GET : method' replaced by 'return method'. Anchor 1->0, blob 4f962dbb6cfd->aa3f8f87edab. Hono unit: 3 failed, 1 passed (the other-verbs control stayed green). Conformance: 13 failed, 11 passed (the controls). The subject resolves from src in both suites (the http-conformance vitest alias of @objectstack/hono), so there is no dist leg.\n2. approvals 'request?.method === HEAD' replaced by 'method === HEAD'. Blob 67a8a4b44bf3->ab7e441de94c. approvals-act.test.ts: 1 failed, 12 passed ('expected HEAD to be GET'). The conformance leg was read on the pre-edit runtime dist (old line count 1, new 0): the HEAD pin was red, hosted 405 vs self-hosted 200. After the rebuild it was green.\nExpected direction: red. Observed: red.",
      "gates": "Local subset at the final head 4f14775b4:\n- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 66 commands, the same list at 0a5693634, 379b734a2 and 4f14775b4.\n- All 66 run at 4f14775b4, each exit captured before any pipe: 66 x exit 0. These include check:dispatcher-error-vocabulary, check:error-status-conformance ('every derivable runtime status is documented, and every documented status is reachable'), check:route-envelope, check:nul-bytes (OK), check:changeset-gate-self-tests, check-changeset-no-major --base origin/main, check:cross-package-test-inputs (OK), check:test-source-alias (OK), check:type-check-coverage, check:type-check-debt and check:dual-build-cjs-loads.\n- --ran reconciliation (as 'cmd :: exit N'): '66 derived famil(ies) accounted for, 66 run, 0 NOT-MEASURED (a DERIVED zero, all 66 recorded an exit code and none of them is 3)'.\n- First pass at 0a5693634: 2 prerequisite refusals (exit 3), both since re-run as above. check-plugin-teardown-shape --self-test needed its pinned fixture commit, fetched with depth 1, and then exited 0 with 48 cases. check:dual-build-cjs-loads needed every workspace dist.\n- ESLint (pnpm exec eslint --no-inline-config) over the 5 changed source and test files: exit 0, no output.\n- check-platform-checklist: not owed, since no export was renamed or deleted (dispatchedMethod is module-private).\nCI, read once after pr_create, without polling: check-runs on 4f14775b4 were 36, of which 16 in_progress, 8 queued, 6 success and 6 skipped. Build Core was queued and Governed Surface Queue Guard in_progress. Lint & Repo Gates, TypeScript Type Check and Test Core were not yet listed. Conclusion: in_progress, NOT MEASURED at report time.",
      "mcp_calls": "0 - no MCP GitHub tool used (reads were REST GETs through the container proxy; writes went through the scripts/pm relay)",
      "api_writes": "2 relay strokes from this session, each a POST /repos/objectstack-ai/objectstack/dispatches (scripts/pm/fleet-write/dispatch.mjs). Stroke 1, pr_create, ran as objectstack-fleet[bot] in run 39110139276: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24286/assignees ['zhuangjianguo']. The read-back said '14071 byte(s) sent, 14071 stored, identical; assignees: zhuangjianguo'. Stroke 2, comment: POST /repos/objectstack-ai/objectstack/issues/24245/comments (this os-dev-report). git push (not REST): the empty branch, then 4 pushes up to 4f14775b4. No labels written (none expected). The card assignee was not written.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a, with contract text (b) beside it. reach: public door, measured in process. createHonoApp + LiteKernel with an mcp slot rendering '# skill', at 4f14775b4; toResponse is untouched by this PR, so it is pre-existing. GET /api/v1/mcp/skill through app.request answers 200 with body 'data: \"# skill\"' plus a blank line: an SSE frame of a JSON string, not the markdown. It also answers content-type 'text/markdown; charset=utf-8, text/markdown; charset=utf-8' (doubled) and cache-control 'no-cache, no-store'. The standalone server serves the raw markdown with cache-control 'no-store' (PR #24244's pin). Seam: runtime:packages/runtime/src/domains/mcp.ts handleMcpSkillRequest stream result ('string events are written verbatim' through every adapter) -> renderer: none; adapter: packages/adapters/hono/src/index.ts toResponse events arm. That arm SSE-frames every event unless vercelDataStream is set, and merges its capitalised Content-Type/Cache-Control with the domain's lowercase ones. Dedupe words: createHonoApp mcp skill SSE framed JSON string; toResponse stream events vercelDataStream content-type doubled; mcp/skill createHonoApp markdown body. Not fixed here (a different defect class); for the seat to file.",
        "carrier: none. Noted, not filed: the auth arm's legacy fallback dispatcher.handleAuth(path, method, ...) still passes the raw verb. handleAuthRequest does not read it (_method), so no answer differs. Recorded in PR Acceptance notes.",
        "carrier: none. Noted, not filed: a HEAD on a GET whose answer is an events stream still runs the stream producer, and Hono discards the body. The standalone server does the same for the same request. Recorded in PR Acceptance notes."
      ],
      "line_budget": "+469 / -9 over 6 files vs origin/main (3-dot). Source: index.ts +40/-2 and approvals.ts +10/-7. Tests: +174 hono unit, +213 conformance, +19 approvals-act. Changeset: +13. Under the 3000-line human-merge threshold.",
      "files_changed": [
        ".changeset/24245-head-dispatched-as-get.md",
        "packages/adapters/hono/src/index.ts",
        "packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts",
        "packages/runtime/src/domains/approvals.ts",
        "packages/runtime/src/domains/approvals-act.test.ts",
        "packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts"
      ],
      "deviations": [
        "File surface widened in the same round, beyond the claim's two candidate homes: packages/runtime/src/domains/approvals.ts plus its test. It is required by the catch-all change, which was measured red without it (hosted HEAD /approvals/act answered 405), and it is declared in the PR body.",
        "The auth arm's /auth/config branch in the same file was fixed under the bounded in-place exemption: same class, mechanical, a file in this claim, same gate family. It was measured 200 vs 404 and pinned by a unit case.",
        "One changeset file lists both published packages as patch (@objectstack/hono, @objectstack/runtime), not one file per package. The file carries the bare Clause-② line, as 70 of 72 existing changesets do.",
        "origin/main (93b4acb57, scripts/pm/dispatch-gates* only) was merged before opening the PR, per AGENTS.md section 10. No package rebuild was needed, and gates were re-derived and re-run on the merged head.",
        "Fetched one pinned fixture commit (git fetch --depth=1 origin 621a4876...) into the shared object store so check-plugin-teardown-shape --self-test could run (it is additive).",
        "The measurement harness was a temporary, uncommitted test file under packages/qa/http-conformance/src, deleted before gate derivation. A copy is kept at the scratchpad path issue-24245/measure-harness.test.ts.txt.",
        "@objectstack/runtime test:repo (vitest repo project) was not run locally; it is CI's Test Core. The diff edits no test in that project.",
        "Commit trailers are the model-free pair the dispatch ordered; the PR body ends with AGENTS.md's session-URL footer. The harness's system reminder asked for a model-named Co-Authored-By and a different PR footer. AGENTS.md (pre-push hook: no model identifier) was followed, and the harness-written forms were not used.",
        "Answered the PM's mid-run status probe with one SendMessage line to the parent."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24286 comment 6247514938 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T08:14Z · the claim surface is amended in place for approvals.ts; out-of-scope finding 1 filed as #24287; landing waits on every check of head 4f14775b4.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24286 → 9822b053e · domain:cli seat 1 · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T08:47Z

    Reading Result
    PR #24286 object merged: true, merge commit 9822b053e, merged 2026-10-18T08:47Z through the merge queue (enqueued 2026-10-18T08:27Z)
    origin/main 9822b053e is an ancestor of origin/main, with a single parent (squash). The 6 files and +469 / −9 match what was reviewed at 4f14775b4
    queue branch no gh-readonly-queue/main/*pr-24286* 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:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething 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