Repository navigation
Commit 1ef0ee8
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)
Fixes #24287
Clause-②: no
Basis for the Clause-② line: a wrong response body and a doubled header
on one adapter are made to match the `result` stream channel's stated
contract (`packages/runtime/src/domains/mcp.ts`: "string events are
written verbatim, custom headers honored") and the standalone server's
answer. No request is newly accepted or refused, and no export moves
(the only source change is inside `toResponse`, a closure in
`createHonoApp`).
## What a caller gets on a `createHonoApp` host
`GET /api/v1/mcp/skill`, measured with the probe's fixture skill (front
matter and a double-quoted heading; the conformance pin's fixture adds
an em dash):
| | before (`fa501123b`, unfixed `index.ts`) | after (`32900fd25`) |
standalone server |
|:--|:--|:--|:--|
| body, `app.request` | `data: "---\nname: objectstack\n---\n\n# skill
\"q\"\n"` plus a blank line (an SSE frame of a JSON string) | the
markdown, byte for byte | the markdown, byte for byte |
| `content-type`, `app.request` | `text/markdown; charset=utf-8,
text/markdown; charset=utf-8` | `text/markdown; charset=utf-8` |
`text/markdown; charset=utf-8` |
| `cache-control`, `app.request` | `no-cache, no-store` | `no-store` |
`no-store` |
| wire header lines, real socket (`@hono/node-server` serving
`app.fetch`) | `Content-Type`, `Cache-Control: no-cache`, `Connection`,
`content-type`, `content-disposition`, `cache-control: no-store` (two
`Content-Type` lines, two `Cache-Control` lines) | one each of
`content-type`, `cache-control: no-store`, `content-disposition`,
`connection` | (pinned in
`mcp-skill-method-not-allowed.hono.integration.test.ts`) |
The real-socket rows come from a one-off probe (a `LiteKernel` with a
fake `mcp` service, `createHonoApp`, `serve({ fetch: app.fetch, port: 0
})`, read with `node:http` `rawHeaders`), run against the built
`@objectstack/hono` dist before and after the fix. It is not a committed
test; the card had that socket as NOT MEASURED.
## Where the fix lives
`packages/adapters/hono/src/index.ts`, `toResponse`'s events arm (the
claim's narrowed home). Measured against the card's two mechanisms:
- **Body.** The arm wrote every event as `data:
${JSON.stringify(event)}` unless `vercelDataStream` was set
(`:835`–`:837` on `21070a2f0`). It now writes a string event as given in
every mode; an object event is still one `data:` frame (one JSON line in
a Vercel data stream). That is the standalone writer's rule
(`runtime/src/dispatcher-plugin.ts`, `sendResultBase`).
- **Headers.** The arm spread the domain's lowercase `content-type` /
`cache-control` into a record beside its own capitalised `Content-Type`
/ `Cache-Control`, and `new Response` appended both. It now seeds a
`Headers` with the same three defaults and `set`s each producer header
over them, so a producer's header replaces the default it names whatever
its case. The standalone writer does the same with `res.header()`.
The domain's channel (`mcp.ts`, held by #24251) was not needed: its
contract was right and this adapter was the one door not honouring it.
## Producers that reach this arm (on `origin/main` `21070a2f0`)
The arm runs for a dispatcher `result` with `type: 'stream'` and
`events`. Only two domains return one:
| producer | events | `vercelDataStream` | answer after this PR |
|:--|:--|:--|:--|
| `/mcp/skill` (`runtime/src/domains/mcp.ts`) | one string (the
markdown) | no | changed: the defect fixed |
| `/ai` routes via `runtime/src/domains/ai.ts`, Vercel data stream
(`service-ai`'s `/ai/chat`, `/ai/agents/:agentName/chat` with its quota
refusal, `/ai/assistant/chat`) | pre-encoded string frames from
`encodeVercelDataStream`, ending `data: [DONE]` | yes | unchanged
(strings were already verbatim in this mode) |
| `/ai` routes via `ai.ts`, plain SSE (`service-ai`'s `/ai/chat/stream`)
| `aiService.streamChat()` parts, which are objects (`TextStreamPart`;
the memory and Vercel adapters both yield objects) | no | unchanged
(objects keep their `data:` frame) |
The `service-ai` rows were read from the published
`@objectstack/service-ai@10.3.0` tarball (`dist/index.js`), since that
package lives outside this repository. Every other `result` producer
(`auth`, `approvals`, `automation-hooks`, `webhooks`) hands a `Response`
and leaves at the arm above. No producer relies on SSE framing of a
string event, so no design question arose.
## Tests
- `packages/adapters/hono/src/hono-stream-events-framing.test.ts`
(fixture dispatcher): the `/mcp/skill` shape answers the markdown
verbatim with each declared header once; object events outside a Vercel
data stream are still SSE frames under the SSE defaults; the `/ai` SSE
shape and the `/ai` Vercel data stream shape answer exactly as before.
-
`packages/qa/http-conformance/src/hono-mcp-skill-raw-body.conformance.test.ts`
(real `LiteKernel`, real `HttpDispatcher`, `@objectstack/hono` aliased
to source): on the hosted door the body is the markdown and
`content-type`, `content-disposition` and `cache-control` each have the
one value the domain declares; the hosted answer equals the standalone
answer (`HonoServerPlugin` + dispatcher plugin over a real socket) in
status, body and those headers.
Runs at `32900fd25`: `pnpm --filter @objectstack/hono test` 215 passed
(11 files), exit 0; `pnpm --filter @objectstack/hono typecheck` exit 0
(the new test file is in `tsconfig.test.json`'s program, counted with
`--listFiles`); `pnpm --filter @objectstack/http-conformance exec vitest
run` over the new file plus `hono-head-as-get` and
`hono-dispatcher-result-response`, 29 passed, exit 0; `pnpm --filter
@objectstack/http-conformance typecheck` exit 0.
## Reverse verification and ablation
- **Before the fix** (`fa501123b`: tests committed, `index.ts` unfixed):
both conformance cases and the unit `/mcp/skill` case red on the body
(`data: "---\nname: objectstack\n…"` received); the three other unit
cases green.
- **Leg A, header merge** (`headers.set(k, String(v))` to
`headers.append(k, String(v))`, through `scripts/ablation-replace.mjs`,
blob `62e6f29d4016` to `e958969acbd3`): unit 3 of 4 red, conformance 2
of 2 red. Direction: more red than the old defect, because `append` also
doubles a same-case header (the `/ai` rows), which the old record spread
did not. Restored: blob back to `62e6f29d4016` and `git diff HEAD`
empty.
- **Leg B, string verbatim** (`const chunk = typeof event === 'string'`
to `const chunk = res.vercelDataStream && typeof event === 'string'`,
blob `62e6f29d4016` to `91002889678f`): unit 1 of 4 red (the
`/mcp/skill` row; the Vercel row stays green), conformance 2 of 2 red.
Restored the same way. A first attempt at this leg was a no-op: its
replacement contained its anchor, `ablation-replace` refused it (anchor
count 1 to 1) and ran no test, so that reading is void and the leg was
rerun with the anchor above.
Both subjects resolve to source (the hono suite imports `./index`;
http-conformance aliases `@objectstack/hono` to `src/index.ts`), so
neither leg needed a rebuild. No ablation file is committed.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `32900fd25` (merge base `30da04f8e`): 66
commands, all run, 66 exit 0. `pnpm check:dual-build-cjs-loads` first
exited 3 (PREREQUISITE NOT MET, no `dist/` for 34 packages); after the
workspace build that `pnpm check:type-check-debt` runs, it was re-run:
exit 0. `--ran` reconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0
UNRUN, every line carrying its exit code. `pnpm exec eslint
--no-inline-config` over the three changed TypeScript files: exit 0, no
output. The repo-wide `pnpm lint`, the type-check lanes and the
path-scheduled CI jobs are CI's.
## Acceptance notes
Observations, not filed (each a reading of code with no producer
exercising it, or an unmeasured inference):
- Remaining differences between this arm and the standalone writer, none
exercised by a producer today: 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; and the `Connection: keep-alive` default stays
when a producer sets other headers (the standalone writer sends only the
producer's). Owner: none.
- `runtime/src/domains/ai.ts` builds its own headers for a stream and
does not pass the AI route's, so on a `createHonoApp` host a Vercel data
stream from `service-ai@10.3.0` would carry `Content-Type: text/plain;
charset=utf-8` and none of the route's `x-vercel-ai-ui-message-stream`,
`x-objectstack-agent` or `x-objectstack-skills`, which the standalone
route mount writes. Read from code and the published tarball only, NOT
MEASURED at a door. Owner: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2b7fc52 commit 1ef0ee8
4 files changed
Lines changed: 266 additions & 7 deletions
File tree
- .changeset
- packages
- adapters/hono/src
- qa/http-conformance/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
820 | 820 | | |
821 | 821 | | |
822 | 822 | | |
823 | | - | |
824 | | - | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
825 | 842 | | |
826 | 843 | | |
827 | 844 | | |
828 | | - | |
829 | | - | |
| 845 | + | |
| 846 | + | |
830 | 847 | | |
831 | 848 | | |
832 | 849 | | |
833 | 850 | | |
834 | 851 | | |
835 | | - | |
836 | | - | |
837 | | - | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
838 | 857 | | |
839 | 858 | | |
840 | 859 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
0 commit comments