Repository navigation
Commit 1aa4dd1
fix(mcp): the record data tools answer a code-less throw as the /data door does, so an undeclared hook abort is HOOK_ABORTED (#31245)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi3qHENwt3pGirMrGoMekDtd)_
Fixes #31218
Clause-②: yes
Metadata-protocol: none
## 维护者速读
- 改了什么:MCP
的六个记录数据工具(`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`)遇到不带错误码的异常时,改用
`/api/v1/data` 同一个分类函数 `mapDataError` 来回答:沙箱 hook 的业务中止回 `HOOK_ABORTED`
+ hook 自己写的那句话;代码 hook 的裸 throw、崩溃、驱动错误回 `500 INTERNAL_ERROR`,原文不再给
agent,改为在服务端打一条 `[5xx]` 日志。带错误码的异常照旧。
- 为什么改:之前 agent 看到的是沙箱调试包装 `hook 'NAME' threw: Error: …`,没有错误码,还暴露 hook
内部名字;同一个中止在 REST 上是 `400 HOOK_ABORTED`。agent 分不清"业务拒绝"和"故障",场景跑分也读不到码。
- 风险与回滚:这六个工具对无码异常的回答从纯文本变成 JSON 信封 `{ error: { code, message, status }
}`,且 5xx 原文被隐去(与 REST 一致);只改 `@objectstack/mcp`,patch。回滚即还原
`mcp-http-tools.ts`。
- 先看:`packages/mcp/src/mcp-http-tools.ts` 的 `dataDoorErrorResult` /
`dataDoorAnswerOf`;`packages/mcp/src/mcp-coded-refusal.test.ts` 末尾的
#31218 块。
- 两分钟验证:`pnpm --filter @objectstack/mcp exec vitest run
src/mcp-coded-refusal.test.ts -t 31218`,预期 16 个用例全部通过。
- 需要你做的:无,席位落地。
## What changes
The record data tools of `packages/mcp/src/mcp-http-tools.ts` rendered a
thrown error that carries no string `code` as its bare message
(`errorResult(messageOf(err))`). A QuickJS hook body's `throw new
Error('…')` arrives as a `SandboxError` with no `code`, whose `.message`
is the `hook 'NAME' threw: Error: …` debug wrapper, so the agent read
the wrapper (hook name included) and no `ErrorCode`, while
`/api/v1/data` answers the same throw `400 HOOK_ABORTED` with the inner
sentence.
- **`dataDoorErrorResult(err, tool, objectName)`**, the catch of
`query_records`, `aggregate_records`, `get_record`, and (through
`landedWriteResultOrError`'s `otherwise`, now a required argument)
`create_record`, `update_record`, `delete_record`:
- a coded throw renders through `thrownRefusalEnvelope` exactly as
before (read first);
- a code-less throw renders the `/data` door's answer, `{ "error": {
"code", "message", "status" } }`, read off `mapDataError(err,
objectName)` (`@objectstack/types`) by `dataDoorAnswerOf`. No second
classification exists in `packages/mcp`;
- a 5xx answer is logged once through `logServerFault` (`[5xx] 500
tools/call update_record task — THROWN-MESSAGE`), since its message no
longer reaches the agent.
- **`rowRefusalOf`** (the per-row grouping of `delete_records`, #30891)
reads the same `dataDoorAnswerOf` instead of its own inline read of
`mapDataError`; its answers are unchanged.
- **`codedErrorResult`** accepts an envelope without `code`, for a throw
that declared a status and no code (the door invents none).
- **No change** to `@objectstack/types`, `packages/spec`, the other
tools, or the coded path.
The family, each answered as the door answers it (pinned at the MCP
door, each asserted literally AND equal to `mapDataError` on the same
throw):
| Throw | Before (MCP) | Now (MCP) = `/data` |
|---|---|---|
| sandboxed hook abort, no code (`SandboxError`, `innerMessage`) | text
`hook 'demoleave_leave_request_guard' threw: Error: Not enough annual
leave remaining` | `HOOK_ABORTED` · 400 · `Not enough annual leave
remaining` |
| the same abort with the `SandboxError` instance lost (wrapper text
only) | the wrapper text | `HOOK_ABORTED` · 400 · inner sentence |
| sandboxed abort that declared `status: 422`, no code | the wrapper
text | `HOOK_ABORTED` · 422 |
| code hook's bare `throw new Error(…)` | its message | `INTERNAL_ERROR`
· 500 · `Internal server error` (message logged) |
| sandboxed body that crashed (`TypeError: …`) | the wrapper text |
`INTERNAL_ERROR` · 500 |
| runtime `callData`'s `{ statusCode: 503, message: 'Data service not
available' }` | its message | no code · 503 · `Internal server error`
(message logged) |
| `{ statusCode: 400, message }`, no code | its message | no code · 400
· its message |
| coded refusal (`RECORD_LOCKED` / 409) — CONTROL | coded envelope |
unchanged, not logged |
## Tests
- `packages/mcp/src/mcp-coded-refusal.test.ts`: a new top-level describe
(#31218), in the shared `/mcp` HTTP harness (`rpcOverHttp`,
`bridgeThrowing`): the undeclared abort for each of the six tools (hook
name and `threw` absent from the text), the lost-instance wrapper, the
declared-4xx abort, the 4xx-is-not-logged pin, the bare throw (message
withheld, one `[5xx]` line naming tool and object), the crash, the 503,
the declared 400 with no code, the coded CONTROL on every data tool, and
a CONTROL that the other tools still answer a code-less throw with its
bare message. The #23509 table's "code-less throw answers its bare
message" row now splits: data tools answer the door's 500, the rest are
unchanged. The "not a code" row moves to `list_actions`, which still
renders that case.
- `packages/mcp/src/mcp-server-runtime.http.test.ts`: two pins that
threw an uncoded `Error` read the new answer. The aggregate pin's
fixture becomes the coded `PermissionDeniedError` the security plugin
really throws for that denial (`security-plugin-authz-middleware.ts`).
<details><summary>Base re-measure (order step 1) and ablation</summary>
Base re-measure: `mcp-http-tools.ts` restored to the base blob
(2b752f1, hash match checked), the #31218 block run: `Tests 13 failed |
3 passed | 46 skipped (62)`. Each data tool's text was `hook 'demo…`
(`SyntaxError: Unexpected token 'h', "hook 'demo"... is not valid JSON`,
×5 tools + the lost-instance row), and the 503 / bare / 400 rows
answered `Data servi…` / `Not enough…` / `aggregate …` as bare text.
Restored with `git checkout HEAD --`, `git diff HEAD` empty.
Ablation (one-time, `node scripts/ablation-replace.mjs`): in
`dataDoorErrorResult`, `return codedErrorResult(door);` replaced by
`return errorResult(messageOf(err));`. `ok mutation landed: anchor 1 ->
0, blob 15cb855 -> 62f6a65d8a2b`; `src/mcp-coded-refusal.test.ts`:
`Tests 19 failed | 43 passed (62)` (the 13 rows above plus the 6 table
rows); `ok restored: blob == HEAD (15cb855) and git diff HEAD is
empty`. The test imports the package's source, so no `dist/` is
involved.
</details>
## Acceptance notes
- **Still answered differently from REST, not changed here (listed for
the seat):**
- a CODED sandboxed refusal (`throw` with `code` + `status` in a hook
body) answers the wrapper text with the hook name, `{ code:
'RECORD_LOCKED', message: "hook 'guard' threw: Error: Locked.", status:
409 }`, measured on `MCPServerRuntime`; `/data` answers the inner
sentence. `thrownRefusalEnvelope` reads `.message`; the order kept the
coded path unchanged.
- the metadata tools (`list_objects`, `describe_object`,
`validate_expression`), `list_actions`, the record resource,
`run_action` / `resume_run` and `delete_records`' outer catch still
render a code-less throw as its bare message. `delete_records`' outer
catch also renders this file's own code-less sentences, which the door's
message heuristics would misread (its "could not read the records"
sentence names the object and contains `not`, the unknown-object limb),
so it keeps `errorResultWithCode`.
- `delete_records`' fault answer still quotes the faulting row's thrown
message.
- a driver's own string `code` (`SQLITE_BUSY`) rides the coded path with
its message, where `/data` answers `500 INTERNAL_ERROR`.
- The 5xx log line is `logServerFault`'s, with `request: { method:
'tools/call', path: 'TOOL OBJECT' }` as its coordinates; the MCP tool
layer has no HTTP request in hand.
## Verification
Merged `origin/main` once before opening (a0f8cfb; the merge touched
`packages/lint` and `plugin-auth` tests only).
- `pnpm --filter @objectstack/mcp test` on a0f8cfb: `Test Files 60
passed (60)`, `Tests 789 passed (789)`, lock `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/mcp typecheck`: exit 0 (includes
`check:test-typecheck`, 53 errors / 8 pinned signatures held).
- `pnpm exec eslint --no-inline-config` on the three touched `.ts`
files: exit 0 (`@objectstack/mcp` has no `lint` script).
<details><summary>Derived gates (node scripts/pm/dispatch-gates.mjs
--commands, 60 commands, all on a0f8cfb)</summary>
- `node scripts/check-adr-0087-registration.mjs --base origin/main` ::
exit 0
- `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0
- `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit
0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0
- `node scripts/check-dts-emitted.mjs --self-test` :: exit 0
- `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0
- `node scripts/check-empty-changeset.mjs --self-test` :: exit 0
- `node scripts/check-issue-citations.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs --self-test` :: exit 0
- `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` :: exit 0
- `node scripts/ci/plan-test-core-shards.mjs --self-test` :: exit 0
- `node scripts/ci/prune-turbo-cache.mjs --self-test` :: exit 0
- `node scripts/ci/snapshot-next.mjs --self-test` :: exit 0
- `node scripts/docs-audit/check-affected-docs.mjs` :: exit 0
- `node scripts/docs-audit/check-drift-comment.mjs` :: exit 0
- `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0
- `node scripts/release-pending-publish.mjs --self-test` :: exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit
0
- `pnpm check:changeset-gate-self-tests` :: exit 0
- `pnpm check:ci-filter-parity` :: exit 0
- `pnpm check:closed-ceremony-text` :: exit 0
- `pnpm check:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:doc-authoring` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:dts-closure` :: exit 0
- `pnpm check:dual-build-cjs-loads` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:error-status-conformance` :: exit 0
- `pnpm check:gitlink-declared` :: exit 0
- `pnpm check:issue-citations` :: exit 0
- `pnpm check:keyed-text-bounds` :: exit 0
- `pnpm check:lean-entry-closure` :: exit 0
- `pnpm check:logger-receiver-detach` :: exit 0
- `pnpm check:nul-bytes` :: exit 0
- `pnpm check:objectql-double-limit` :: exit 0
- `pnpm check:org-identifier` :: exit 0
- `pnpm check:page-declaration-shape` :: exit 0
- `pnpm check:platform-object-tenancy-census` :: exit 0
- `pnpm check:published-files` :: exit 0
- `pnpm check:query-options-erasure` :: exit 0
- `pnpm check:refd-timer-probe` :: exit 0
- `pnpm check:registry-log-declared` :: exit 0
- `pnpm check:rest-log-spy-declared` :: exit 0
- `pnpm check:slot-lookup` :: exit 0
- `pnpm check:sourcemap-no-sources-content` :: exit 0
- `pnpm check:system-context-census` :: exit 0
- `pnpm check:test-source-alias` :: exit 0
- `pnpm check:tier-file-adoption` :: exit 0
- `pnpm check:type-check-coverage` :: exit 0
- `pnpm check:type-check-debt` :: exit 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:vouch-for-reference-callers` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
`node scripts/pm/dispatch-gates.mjs --ran`: `Run reconciliation — 60
derived, 60 run, 0 NOT-MEASURED, 0 UNRUN.`
Not run locally (CI's): `test:repo`, the dogfood shards (queue only),
the type-check lanes.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the touched
files.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ihL2eNMbdYCpXSovm5QP5)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 169ff7e commit 1aa4dd1
4 files changed
Lines changed: 318 additions & 33 deletions
File tree
- .changeset
- packages/mcp/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
| |||
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
87 | 93 | | |
88 | 94 | | |
89 | 95 | | |
90 | 96 | | |
91 | 97 | | |
92 | 98 | | |
93 | 99 | | |
94 | | - | |
| 100 | + | |
95 | 101 | | |
96 | 102 | | |
97 | 103 | | |
98 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
99 | 114 | | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | 115 | | |
104 | | - | |
| 116 | + | |
105 | 117 | | |
106 | 118 | | |
107 | 119 | | |
| |||
178 | 190 | | |
179 | 191 | | |
180 | 192 | | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
185 | 210 | | |
186 | 211 | | |
187 | 212 | | |
| |||
201 | 226 | | |
202 | 227 | | |
203 | 228 | | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
204 | 232 | | |
205 | 233 | | |
206 | 234 | | |
| |||
242 | 270 | | |
243 | 271 | | |
244 | 272 | | |
245 | | - | |
| 273 | + | |
246 | 274 | | |
247 | 275 | | |
248 | 276 | | |
| |||
254 | 282 | | |
255 | 283 | | |
256 | 284 | | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
257 | 427 | | |
258 | 428 | | |
259 | 429 | | |
| |||
0 commit comments