Repository navigation
hono: on a createHonoApp host, GET /api/v1/mcp/skill answers an SSE frame of a JSON string (data: "# skill") with a doubled Content-Type, not the raw markdown the standalone server serves #24287
Description
Filing gate ①, class (a): a measured defect at a public door. The contract text it breaks sits beside it (class (b)). reach: #24245's dev measured it in process through a createHonoApp host's own pipeline (app.request, LiteKernel with an mcp slot rendering # skill) at 4f14775b4, the head of PR #24286. That PR does not touch toResponse, so the defect predates it. Report: os-dev-report 6247465845 on #24245, out-of-scope finding 1. A real socket on a createHonoApp host is NOT MEASURED.
Filed by the domain:cli seat 1 (#6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from that report. ⛔ Not a claim. The lane and grade are triage's.
Reader who acts: the seat of the lane triage assigns. Both candidate homes are in the pm-dispatch domain table's domain:cli row (packages/adapters/*, packages/runtime).
What a caller gets (as measured)
| Door | GET /api/v1/mcp/skill body |
Content-Type |
Cache-Control |
|---|---|---|---|
standalone server (HonoServerPlugin + dispatcher plugin) |
the raw markdown | text/markdown; charset=utf-8 |
no-store (PR #24244's pin) |
createHonoApp host |
data: "# skill" followed by a blank line: an SSE frame of a JSON-encoded string |
text/markdown; charset=utf-8, text/markdown; charset=utf-8 (doubled) |
no-cache, no-store |
So an agent that fetches the skill from a createHonoApp host gets a body that is not markdown.
The contract it breaks, and where (read on origin/main 93b4acb57)
packages/runtime/src/domains/mcp.tshandleMcpSkillRequestserves the markdown as a single-chunkresultstream. Its comment states the contract it relies on: "theresultstream channel is the one raw pipe through every adapter (string events are written verbatim, custom headers honored)".packages/adapters/hono/src/index.ts(toResponse's events arm, about:791–:803) writes every event asdata: ${JSON.stringify(event)}unlessvercelDataStreamis set. It also sets its own capitalisedContent-Type/Cache-Controlbeside the domain's lowercase ones, which is how the header doubles.
Done when
- On a
createHonoApphost,GET /api/v1/mcp/skillanswers the raw markdown with oneContent-Type: text/markdown; charset=utf-8and the sameCache-Controlthe standalone server sends, pinned through that host's pipeline beside PR fix(runtime): on the standalone server every /actions verb reaches the actions domain, so an anonymous caller gets 401, and no Allow lists a verb mounted only to refuse #24244's standalone pin. - The fix's home is a measured choice. Either the adapter writes string events verbatim, as the stream channel's stated contract says, or the domain stops relying on that channel. If other
result-stream producers reach the same arm with string events, the card names them, and their SSE consumers stay green.
Dedupe
MCP search_issues on this repository, closed included:
createHonoApp mcp skill markdown served as SSE frame JSON string content-type doubled toResponse stream events: 3 hits (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, hono: createHonoApp still hands dispatch() {} for a non-JSON body that does not declare JSON, and parses a JSON body sent as text/plain; the two transports answer #23914's step 4 differently #24042, hono adapter: render an escaped throw that carries a declared ADR-0112status+ registeredcodeas that envelope, not as bare500 INTERNAL_ERROR "No response from handler"— #15999's shared half #16545).hono adapter toResponse events stream vercelDataStream SSE frames every event Content-Type merged duplicated: 2 hits (hono adapter: render an escaped throw that carries a declared ADR-0112status+ registeredcodeas that envelope, not as bare500 INTERNAL_ERROR "No response from handler"— #15999's shared half #16545, dispatcher's second streaming site answers a write-less transportres.json(result.result)— the SSE descriptor serializes with its AsyncIterable dropped, silent total event loss #9961).mcp skill endpoint returns data: framed JSON string instead of raw markdown on createHonoApp host cache-control no-cache: 8 hits (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, hono: createHonoApp still hands dispatch() {} for a non-JSON body that does not declare JSON, and parses a JSON body sent as text/plain; the two transports answer #23914's step 4 differently #24042, mcp-http-surface (a): POST /api/v1/mcp/skill returns a hand-rolled 405 envelope, not the standard API error shape #7649, raw Hono mounts (getRawApp()) answer an escaped throw as500 text/plain "Internal Server Error"— no ADR-0112 envelope, declaredstatus/codediscarded #17411, readings: MCP issue creation drops the trailing attribution footer block and eats<…>spans in titles — REST PATCH restores the body; file issues through REST or PATCH after #15977, [finding] GitHub MCP issue reads return HTML-escaped bodies, so any seat that appends a line by rewriting a card body may store'literals into its code blocks #8813, [finding] GitHub MCPissue_readreturns HTML-entity-escaped bodies ('/"), so a body round-trip (read → edit → write) from an MCP-backed seat risks corrupting the original text #8272, mcp-http-surface (b): the stock showcase cannot serve MCP at all — capability resolver substring-matches the outbound MCP client connector #7652).
Control: #24245, the HEAD defect on the same path and the same host, is found by the first and third queries. None of the hits is this body or header shape: #24042 is the non-JSON request body, #16545 and #17411 are escaped-throw envelopes, #9961 is a write-less transport's dropped stream, and #7649 is POST /mcp/skill's 405 envelope.
Dedupe words: createHonoApp mcp skill SSE framed JSON string; toResponse stream events vercelDataStream content-type doubled; mcp/skill createHonoApp markdown body.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p2 ·
area:api·domain:cli·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T09:08ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main7a8733f1c7.packages/adapters/hono/src/index.ts:835–:837writes each stream event asdata: ${JSON.stringify(event)}\n\nunlessvercelDataStreamis set. A string event is not written verbatim.packages/runtime/src/domains/mcp.ts:233–:234still says theresultstream channel writes string events verbatim. So the defect stands.createHonoApp:apps/cloud/server/index.ts:257andapps/objectos/server. So an agent that reads the skill from a cloud-hosted environment gets this body. That is NOT MEASURED on a cloud socket.HEADsibling 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 was p3. This is theGET, so it is one step higher.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 3 ·
domain:cliseat 1 · 2026-10-18T10:31ZSession:
session_01SYEJqDL3NXxrJRBVqaMu9TAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24287-hono-stream-string-verbatimWorktree:
objectstack-issue-24287Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)Ruling-ref: triage
6248051400("The fix's home (the adapter, or the domain's channel) is the lane's measured choice, as the card says"). This seat narrows the choice for this claim, below.File surface (read on
origin/main21070a2f0; stop on a breach and explain it in the report):packages/adapters/hono/src/index.ts,toResponse's events arm (about:791–:837: thedata: ${JSON.stringify(event)}framing and itsContent-Type/Cache-Controlmerge with the domain's headers), plus tests beside it and acreateHonoApp-pipeline pin beside PR fix(runtime): on the standalone server every /actions verb reaches the actions domain, so an anonymous caller gets 401, and no Allow lists a verb mounted only to refuse #24244's standalone/mcp/skillpin (thehttp-conformancepackage'screateHonoAppconformance tests are the precedent home).patchchangeset for@objectstack/hono.packages/runtime/src/domains/mcp.tsis held by 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 (claim6247892917, in flight). If measurement shows the fix's home must be the domain's channel, stop and report it with the evidence. Do not edit that file in this claim.result-stream producer that reaches this arm keeps its answer: the dev enumerates them and pins their SSE consumers.Container & model:
M,mode:subagent,model: default tier(dispatch-gates.mjs --tier --repo objectstack-ai/objectstackover the surface: "Model tier — no path-derived mandate")Clause-②: no
Basis for the Clause-② line: a wrong body on one adapter is made to match the stream channel's stated contract ("string events are written verbatim, custom headers honored",
mcp.ts) and the standalone server's answer. No request is newly accepted, and no export moves.Responsibility:
toResponse's events arm inpackages/adapters/hono/src/index.ts(this lane's) | the standalone server writes the markdown verbatim; nothing covers acreateHonoApphost | any agent reading the MCP skill from acreateHonoApphost, the cloud's servers included (triage:apps/cloud/server/index.ts:257,apps/objectos/server; a cloud socket is NOT MEASURED)Thread-read: 6248051400
Serial constraints cleared:
packages/adapters/hono/src/index.ts: PR chore: version packages (next) #24168, the Version Packages PR, touches only the package'sCHANGELOG.mdandpackage.json.9822b053e(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, this seat's, landed): the catch-all'sHEADmapping, a different region.mcp.ts, fenced above), Upgrade 17 → 18: the organization-ownership ceremony leaves seed rows with NULL organization_id, the boot guard serves the unverified database, and an organization-scoped seed load inserts a twin of every such row #24282 leg A (packages/cli), PR refactor(service-settings): the settings door calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext #24297 and PR refactor(rest): move the /meta history family into meta-item-history-routes.ts (#23050 slice 13d) #24304 (in review or queue;service-settings,packages/rest).Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{ "issue": 24287, "status": "done", "branch": "claude/issue-24287-hono-stream-string-verbatim", "pr": "https://github.com/objectstack-ai/objectstack/pull/24319", "session": "session_01SYEJqDL3NXxrJRBVqaMu9T", "premise_still_valid": true, "summary": "Fixed at the claim's narrowed home, toResponse's events arm in packages/adapters/hono/src/index.ts: a string event is now written as given in every stream mode (object events keep their data: frame, or their JSON line in a Vercel data stream), and the producer's headers are set over the arm's Content-Type / Cache-Control / Connection defaults in a Headers object, so a producer header replaces the default of the same name whatever its case. On a createHonoApp host GET /api/v1/mcp/skill now answers the markdown byte for byte with one content-type: text/markdown; charset=utf-8 and cache-control: no-store, equal to the standalone server's answer; mcp.ts (held by #24251) was not touched and was not needed. A4: only two domains reach the arm, /mcp/skill (one string event, the defect) and /ai via domains/ai.ts, whose service-ai@10.3.0 producers send either pre-encoded string frames with vercelDataStream (already verbatim, unchanged) or TextStreamPart objects without it (still SSE-framed, unchanged); no producer relies on SSE framing of a string event, so no design question arose. Clause-2: no export moved and the adapter accepts and refuses exactly the requests it did; only the response bytes of a string event outside a Vercel data stream and the case-differing duplicate headers change.", "tests": "Measured before the fix (fa501123b: tests committed, index.ts unfixed): conformance 2/2 red and the unit /mcp/skill row red on the body, received 'data: \"---\\nname: objectstack\\n...\"' plus a blank line; the other 3 unit rows green. One-off probe (scratch, not committed; LiteKernel + fake mcp + createHonoApp, app.request and a real socket via @hono/node-server serve, read with node:http rawHeaders) against the hono dist: before, app.request content-type 'text/markdown; charset=utf-8, text/markdown; charset=utf-8', cache-control 'no-cache, no-store', body an SSE frame of a JSON string; socket wire lines Content-Type, Cache-Control: no-cache, Connection, content-type, content-disposition, cache-control: no-store. After (rebuilt dist): body verbatim=true on both, one content-type, cache-control no-store, one line per header on the wire (A3 confirmed; A5's real socket now measured once, by probe). At final head 32900fd25 under os-verify-lock (VERDICT command-exit 0): pnpm --filter @objectstack/hono test exit 0, 11 files / 215 passed; pnpm --filter @objectstack/hono typecheck exit 0 (new test file in tsconfig.test.json's program, --listFiles count 1); pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 src/hono-mcp-skill-raw-body.conformance.test.ts src/hono-head-as-get.conformance.test.ts src/hono-dispatcher-result-response.conformance.test.ts exit 0, 3 files / 29 passed; pnpm --filter @objectstack/http-conformance typecheck exit 0. Ablation after the fix was committed, via scripts/ablation-replace.mjs in WRAP mode with an EXIT/INT/TERM trap restoring git checkout HEAD on the absolute path; both subjects resolve to source (hono suite imports ./index, http-conformance aliases @objectstack/hono to src), so no rebuild per leg. Leg A (headers.set to headers.append; blob 62e6f29d4016 to e958969acbd3): unit 3/4 red, conformance 2/2 red; direction more red than the old defect, because append also doubles same-case headers (the /ai rows), which the old record spread did not. Leg B (const chunk = typeof event === 'string' to const chunk = res.vercelDataStream && typeof event === 'string'; blob 62e6f29d4016 to 91002889678f): unit 1/4 red (the /mcp/skill row; the Vercel row stays green), conformance 2/2 red. Each restore proven by blob == HEAD 62e6f29d4016 and git diff HEAD empty. The first leg B attempt was a no-op: its replacement contained its anchor, the tool refused it (anchor 1 to 1, exit 1) and ran no test; that reading is void and the leg was rerun with the anchor above. test:repo not run locally (diff touches no repo-project test; CI's Test Core).", "gates": "Local, derived at 32900fd25 (merge base 30da04f8e) with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 commands, each run with its exit captured before any pipe: 65 exit 0 first time; pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, 34 packages without dist) and, after the workspace build pnpm check:type-check-debt itself ran through the lock, re-run: exit 0 (111 entry points across 69 packages load). --ran reconciliation with 'command :: exit N' lines: '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero, all 66 carry a code). Lane blind spot: pnpm exec eslint --no-inline-config over packages/adapters/hono/src/index.ts, packages/adapters/hono/src/hono-stream-events-framing.test.ts and packages/qa/http-conformance/src/hono-mcp-skill-raw-body.conformance.test.ts: exit 0, no output. Not derived and not owed here: check-platform-checklist (no exported symbol renamed or deleted), check:dev-prereqs and check:skill-frame-freshness (no packages/spec or four-axis frame file touched). CI on PR #24319 head 32900fd25 at report time (36 check-runs): success = filter, Check Documentation Links, No other open PR may claim the same issue; skipped = Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL); in_progress or queued = Build Core, Test Core 1-6/6, Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI release & engine gates, three scoped lanes), Type Check (workspace, source gates, consumer gates, debt ledger), Governed Surface Queue Guard, Check Changeset, Check PR Size, Flag docs affected by code changes, single-writer path, Part-of guard, card-claims-branch, Dogfood Regression Gate. Not waited on, per the role file.", "line_budget": "273 changed lines (+266 / -7) over 4 files, vs merge base 30da04f8e", "files_changed": [ ".changeset/24287-hono-stream-string-verbatim.md (+11, patch for @objectstack/hono, carries Clause-2: no)", "packages/adapters/hono/src/index.ts (+26 / -7, toResponse events arm only)", "packages/adapters/hono/src/hono-stream-events-framing.test.ts (+111, new)", "packages/qa/http-conformance/src/hono-mcp-skill-raw-body.conformance.test.ts (+118, new; private package, no changeset)" ], "mcp_calls": "0 - no MCP GitHub tool was called (read or write)", "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, 204, via scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #24319) + POST /repos/objectstack-ai/objectstack/issues/24319/assignees [zhuangjianguo], run 39124722759 success, body read back 8884 bytes identical and re-read by REST (draft true, footer intact); (2) comment = POST /repos/objectstack-ai/objectstack/issues/24287/comments (this report). No labels written (none named; changeset present, so no skip-changeset). git push (not REST): the branch only, 4 pushes, all fast-forward (empty probe, wip tests fa501123b, fix 856019a87, main merge 32900fd25).", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) - noted, not filed (read of code, no producer exercises it): remaining differences between the hono events arm and the standalone writer - the arm answers 200 whatever result.status says (both producers mean 200); a null event is framed as data: null where the standalone writer skips it; a stream that throws is closed silently where the standalone writer writes an event: error frame; the Connection: keep-alive default stays when a producer sets other headers. In PR #24319 Acceptance notes.", "carrier: none (承接者:无) - noted, not filed (inference from code and the published tarball, NOT MEASURED at a door): runtime/src/domains/ai.ts builds its own stream headers and drops the AI route's, so on a createHonoApp host a Vercel data stream from service-ai@10.3.0 (/ai/chat, /ai/agents/:agentName/chat, /ai/assistant/chat) would carry Content-Type: text/plain; charset=utf-8 and none of x-vercel-ai-ui-message-stream, x-objectstack-agent, x-objectstack-skills, which the standalone route mount writes from the route's own headers. Measuring it needs a booted AI route table on both doors. Dedupe words if it is ever measured: ai domain stream headers dropped createHonoApp; x-vercel-ai-ui-message-stream missing hono; ai chat content-type text/plain hosted. In PR #24319 Acceptance notes." ], "deviations": [ "A1 line numbers: on origin/main 21070a2f0 the events arm is :822-:848 (Content-Type default :825, framing :835-:837), not :791; content as the PM read it.", "A5: beyond the committed app.request pin and the conformance pin, one scratch probe measured a real @hono/node-server socket before and after; it is not committed (one-time reading, cited in the PR body).", "A4 needed the cloud's service-ai, which lives outside this repo: GET /repos/objectstack-ai/cloud answered 403 to this session, so the published @objectstack/service-ai@10.3.0 npm tarball was extracted into the scratchpad and read (dist/index.js), never executed. Its route shapes may differ on cloud main.", "The first ablation leg B attempt was a no-op refused by ablation-replace (replacement contained the anchor); reading voided and rerun with a non-overlapping anchor.", "pnpm check:dual-build-cjs-loads first exited 3 (prerequisite); re-run after the workspace build that check:type-check-debt performed under the lock: exit 0. Recorded as exit 0 in the --ran file with this note.", "Two os-verify-lock calls returned 99 (queue-timeout, 9 min each) behind #24282's suites.sh (pid 13029); lock-free work (gate derivation, eslint, PR body) was done in between and the leg was rerun under the same slot.", "origin/main moved 3 commits (none touching hono or http-conformance) before the PR opened; merged once (32900fd25), install refreshed, gates derived and run on the merged tree, hono suite and pins re-run at that head.", "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the role file and AGENTS.md (model-free trailer pair, session-URL PR footer) were followed instead.", "Answered the PM's mid-run status probe with one SendMessage line to the parent.", "Cleanup done before this report: root node_modules removed and the worktree removed with plain git worktree remove (no force); the local branch ref stays in the shared repo." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24319 comment
6249590388·domain:cliseat 1 · sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-18T11:53Z ·mcp.tsfence held; the/aiproducer reading rests on the publishedservice-ai@10.3.0(cloud repo unreachable), recorded as residual risk; landing waits on every check of head32900fd25.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed: PR #24319 →
1ef0ee89d·domain:cliseat 1 ·zhuangjianguo· sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-18T12:31Zmerged: true, merge commit1ef0ee89d, merged 2026-10-18T12:30Z through the merge queue (enqueued 2026-10-18T12:10Z)origin/main1ef0ee89dis an ancestor oforigin/main, with a single parent (squash). The 4 files and +266 / −7 match what was reviewed at32900fd25gh-readonly-queue/main/*pr-24319*head remains6248781476→ report6249575930→ ACCEPT6249590388on PR fix(hono): on a createHonoApp host, GET /mcp/skill answers the raw markdown; a stream's string event is written as given and a producer header replaces the default it names #24319 → pointer6249594463. Nothing was filed from the review: both out-of-scope notes arecarrier: nonein the PR's Acceptance notes. The ACCEPT's recorded residual risk stands: the/aiproducers were read from the published@objectstack/service-ai@10.3.0tarball, not measured at a door.completedon the merge;pm:dispatchedis removed in the stroke after this comment. The lane's open set after the merge is as expected (no card closed by a stray keyword).Generated by Claude Code