You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP query_dataset with an empty selection.measures reaches the SQL driver and answers 500 DATABASE_ERROR, while the REST twin refuses it 400 #33028
Reproduction (stock showcase at ee9ad83, keyed MCP session over /api/v1/mcp with an admin osk_ key; reproduced twice)
tools/call query_dataset {datasetName:"showcase_invoice_metrics", selection:{measures:[]}} → isError{error:{code:"DATABASE_ERROR", status:500, message:"The database refused to run a raw statement…"}}. Server log: [sql-driver] DATABASE_ERROR … SQLITE_ERROR … near "FROM": syntax error.
With measures:[] and dimensions:["status"], the tool serves group rows that have no measures.
REST twin: POST /api/v1/analytics/dataset/query with the same selection → 400 VALIDATION_FAILED "body.selection.measures must be a non-empty array of measure names."
Expected: the MCP door refuses the empty measure list before dispatch, with the same answer as the REST door; never a 500.
Mechanism
The MCP input takes DatasetSelectionSchema by reference (packages/spec/src/api/analytics.zod.ts:633, measures: shared.measures), and that schema accepts an empty list. The REST route adds its own non-empty check (packages/rest/src/analytics-routes.ts:489), which the MCP door lacks. The executor therefore builds a SELECT with no projection.
Fix direction: put the non-empty rule in the shared selection schema, or give the MCP input the same guard, and pin it in packages/mcp/src/mcp-dataset-tools.test.ts.
QA-source: #33025 · ai.mcp-dataset-tools · observed beside the clauses
Line: an agent over MCP query_dataset with an empty selection.measures · any dataset · every such call · measured twice.
Grade: p2. The tool reaches the SQL driver and answers 500 DATABASE_ERROR with raw driver text, where the REST twin answers 400 VALIDATION_FAILED. It is a loud failure and the agent corrects it by passing a measure, so it is not a wrong answer or a stuck agent, and the MCP raise does not apply. The validation gap and the leaked driver text are the fix. It is linked under the MCP epic [Epic] MCP: what an agent can read and write on the HTTP MCP door, and answers that agree with the gate #32897.
Claim: PM loop round 3, card thread · 2026-11-13T15:53Z · MCP p2 under the maintainer's 「mcp 相关 p2 任务也可以派发处理」 (2026-11-13T15:50:52Z)
Session: session_01L9qibvxW3CnsR8FmFCXFQn
Account: os-tesla
Branch: claude/project-thread-mbqhe2
Worktree: objectstack-issue-33028
Domain: domain:server
Seat: domain:server#7
File surface: packages/mcp/src/mcp-http-tools.ts (the query_dataset input check) or the dataset tool module it delegates to, packages/rest/src/analytics-routes.ts only to export and share the existing non-empty check, their tests, one root changeset. ⛔ No packages/spec change. Stop on breach.
Container & model: S, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; PM default), effort: medium
Clause-②: no (narrowing) — an empty measure list that answered 500 at the MCP door is refused 400 like the REST door
Metadata-protocol: none
Responsibility: the 500 comes from the MCP query_dataset door in packages/mcp (objectstack) | the REST route's guard already covers its own door | any MCP agent sending an empty measure list reaches it today
Thread-read: 6845330955
Premise: holds (reproduced twice by qa#3 at ee9ad834; the dev reproduces at origin/main first)
Serial constraints cleared: mcp-http-tools.ts is shared with #32442 (server#5) and this seat's #32872, #32686 (PR #33123), #33107: edits stay on the query_dataset path; land serially, merge main after a sibling lands
Claim amendment to this seat's claim 6857989541 on #33028 · 2026-11-13T15:55Z: the file surface adds
packages/qa/dogfood/test/dataset-package-scoped-readers.dogfood.test.ts: one case sends measures: [] to POST /api/v1/analytics/dataset/query and to MCP query_dataset as one caller, and asserts the same code, status and message (the agreement pin, following the house precedent of serving both doors side by side);
packages/services/service-analytics/src/dataset-tools.ts (queryDataset) and its test, IF the guard can sit at that shared executor. The executor also serves the product-AI chat tool query_dataset, which lacks the same guard (same defect class, same seam, unclaimed). One guard there covers both faces. packages/rest/src/analytics-routes.ts is dropped from the surface: the two packages share no dependency, so there is nothing to export.
{
"issue": 33028,
"status": "done",
"branch": "claude/project-thread-mbqhe2",
"pr": "https://github.com/objectstack-ai/objectstack/pull/33141",
"session": "session_01L9qibvxW3CnsR8FmFCXFQn",
"premise_still_valid": true,
"summary": "query_dataset now refuses a selection with measures: [] (with or without dimensions) 400 VALIDATION_FAILED with the REST door's sentence ('body.selection.measures must be a non-empty array of measure names.'), before the dataset is read. Per the seat's ruling and claim amendment 6858058804 the guard sits in the shared service-analytics executor (dataset-tools.ts queryDataset), so it covers the MCP tool and the product-AI chat tool (execute) in one place; the MCP door relays the thrown code/status/message whole (thrownRefusalEnvelope), so packages/mcp is unchanged. packages/rest and packages/spec are untouched. The REST/MCP agreement is pinned by one dogfood case per selection shape, since rest and service-analytics/mcp share no runtime dependency.",
"tests": "Measure at 7c36f524: new dataset-tools.test.ts pins 2 failed | 26 passed (28), both measures: [] cases SERVED (answer object, rowCount 1). At 4beb312f: dataset-tools.test.ts 28/28; service-analytics test 215 files, 5187 passed | 332 skipped; typecheck exit 0 (--listFiles includes the test); dataset-package-scoped-readers.dogfood.test.ts 18/18. Ablation A1 (source, ablation-replace anchor 'if (raw.selection.measures.length === 0) throw' -> 'if (false) throw', blob 788afd5b -> c09a6df0): unit 2 failed | 26 passed; restored blob == HEAD, git diff HEAD empty. Ablation A2 (dist, replacement '... === -7) throw', blob -> befa3942, service-analytics rebuilt, ablation-dist-preflight exit 0): dogfood file 2 failed | 16 passed, MCP answered DATABASE_ERROR (sql-driver 'near \"FROM\": syntax error') without dimensions and isError false with dimensions, the card's two observations; restored, rebuilt, preflight --absent exit 0.",
"gates": "dispatch-gates --commands at 4beb312f: 66 families, record /mnt/project-files/server-seat-7/r2026-11-13/out/33028-gates-ran-4beb312f.list; --ran: 66 accounted, 65 run, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, dist of packages outside the closure; self-test 93 cases passed). check-plugin-teardown-shape --self-test first exit 3 (shallow), after fetching 621a4876 exit 0. pnpm lint exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "4beb312f · os-verify-lock VERDICT command-exit 0 · Test Files 460 passed | 3 skipped (463) · Tests 3876 passed | 42 skipped (3918) · no failures",
"mcp_calls": "4 — reads only: issue_read get x1, issue_read get_comments x2, pull_request_read get x1 (PR body precedent); 0 write tools",
"api_writes": "2 — relay pr_create (POST pulls + assignees os-tesla) via fleet-write dispatch run 43471347448; this os-dev-report comment via post-stamped. Plus git push (not a REST write).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"Other doors to the executor's empty projection (order step 3): REST single + batch go through answerDatasetQuery, which already refuses; MCP query_dataset and the chat tool query_dataset go through dataset-tools.ts, both fixed here. No other caller passes a caller selection to AnalyticsService.queryDataset. carrier: none",
"Wording: the tools now answer 'body.selection.measures ...' (the REST sentence, by the parity rule) although a tool input has no body; rewording means changing both doors together. carrier: none, noted in Acceptance notes"
]
}
Activity
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: first touch →
domain:server· p2 ·pm:queue· 2026-11-13T05:48Z⛔ Not a claim.
query_datasetwith an emptyselection.measures· any dataset · every such call · measured twice.DATABASE_ERRORwith raw driver text, where the REST twin answers 400VALIDATION_FAILED. It is a loud failure and the agent corrects it by passing a measure, so it is not a wrong answer or a stuck agent, and the MCP raise does not apply. The validation gap and the leaked driver text are the fix. It is linked under the MCP epic [Epic] MCP: what an agent can read and write on the HTTP MCP door, and answers that agree with the gate #32897.Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim: PM loop round 3, card thread · 2026-11-13T15:53Z · MCP p2 under the maintainer's 「mcp 相关 p2 任务也可以派发处理」 (2026-11-13T15:50:52Z)
Session:
session_01L9qibvxW3CnsR8FmFCXFQnAccount:
os-teslaBranch:
claude/project-thread-mbqhe2Worktree:
objectstack-issue-33028Domain:
domain:serverSeat:
domain:server#7File surface:
packages/mcp/src/mcp-http-tools.ts(thequery_datasetinput check) or the dataset tool module it delegates to,packages/rest/src/analytics-routes.tsonly to export and share the existing non-empty check, their tests, one root changeset. ⛔ Nopackages/specchange. Stop on breach.Container & model:
S,mode:thread,model: default (dispatch-gates --tier: no path-derived mandate; PM default),effort: mediumClause-②: no (narrowing) — an empty measure list that answered 500 at the MCP door is refused 400 like the REST door
Metadata-protocol: none
Responsibility: the 500 comes from the MCP
query_datasetdoor inpackages/mcp(objectstack) | the REST route's guard already covers its own door | any MCP agent sending an empty measure list reaches it todayThread-read: 6845330955
Premise: holds (reproduced twice by qa#3 at
ee9ad834; the dev reproduces atorigin/mainfirst)Serial constraints cleared:
mcp-http-tools.tsis shared with #32442 (server#5) and this seat's #32872, #32686 (PR #33123), #33107: edits stay on thequery_datasetpath; land serially, merge main after a sibling landsobjectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim amendment to this seat's claim 6857989541 on #33028 · 2026-11-13T15:55Z: the file surface adds
packages/qa/dogfood/test/dataset-package-scoped-readers.dogfood.test.ts: one case sendsmeasures: []toPOST /api/v1/analytics/dataset/queryand to MCPquery_datasetas one caller, and asserts the same code, status and message (the agreement pin, following the house precedent of serving both doors side by side);packages/services/service-analytics/src/dataset-tools.ts(queryDataset) and its test, IF the guard can sit at that shared executor. The executor also serves the product-AI chat toolquery_dataset, which lacks the same guard (same defect class, same seam, unclaimed). One guard there covers both faces.packages/rest/src/analytics-routes.tsis dropped from the surface: the two packages share no dependency, so there is nothing to export.objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibvxW3CnsR8FmFCXFQn
os-dev-report
{ "issue": 33028, "status": "done", "branch": "claude/project-thread-mbqhe2", "pr": "https://github.com/objectstack-ai/objectstack/pull/33141", "session": "session_01L9qibvxW3CnsR8FmFCXFQn", "premise_still_valid": true, "summary": "query_dataset now refuses a selection with measures: [] (with or without dimensions) 400 VALIDATION_FAILED with the REST door's sentence ('body.selection.measures must be a non-empty array of measure names.'), before the dataset is read. Per the seat's ruling and claim amendment 6858058804 the guard sits in the shared service-analytics executor (dataset-tools.ts queryDataset), so it covers the MCP tool and the product-AI chat tool (execute) in one place; the MCP door relays the thrown code/status/message whole (thrownRefusalEnvelope), so packages/mcp is unchanged. packages/rest and packages/spec are untouched. The REST/MCP agreement is pinned by one dogfood case per selection shape, since rest and service-analytics/mcp share no runtime dependency.", "tests": "Measure at 7c36f524: new dataset-tools.test.ts pins 2 failed | 26 passed (28), both measures: [] cases SERVED (answer object, rowCount 1). At 4beb312f: dataset-tools.test.ts 28/28; service-analytics test 215 files, 5187 passed | 332 skipped; typecheck exit 0 (--listFiles includes the test); dataset-package-scoped-readers.dogfood.test.ts 18/18. Ablation A1 (source, ablation-replace anchor 'if (raw.selection.measures.length === 0) throw' -> 'if (false) throw', blob 788afd5b -> c09a6df0): unit 2 failed | 26 passed; restored blob == HEAD, git diff HEAD empty. Ablation A2 (dist, replacement '... === -7) throw', blob -> befa3942, service-analytics rebuilt, ablation-dist-preflight exit 0): dogfood file 2 failed | 16 passed, MCP answered DATABASE_ERROR (sql-driver 'near \"FROM\": syntax error') without dimensions and isError false with dimensions, the card's two observations; restored, rebuilt, preflight --absent exit 0.", "gates": "dispatch-gates --commands at 4beb312f: 66 families, record /mnt/project-files/server-seat-7/r2026-11-13/out/33028-gates-ran-4beb312f.list; --ran: 66 accounted, 65 run, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, dist of packages outside the closure; self-test 93 cases passed). check-plugin-teardown-shape --self-test first exit 3 (shallow), after fetching 621a4876 exit 0. pnpm lint exit 0. CI check-runs: in_progress at report time.", "dogfood_battery": "4beb312f · os-verify-lock VERDICT command-exit 0 · Test Files 460 passed | 3 skipped (463) · Tests 3876 passed | 42 skipped (3918) · no failures", "mcp_calls": "4 — reads only: issue_read get x1, issue_read get_comments x2, pull_request_read get x1 (PR body precedent); 0 write tools", "api_writes": "2 — relay pr_create (POST pulls + assignees os-tesla) via fleet-write dispatch run 43471347448; this os-dev-report comment via post-stamped. Plus git push (not a REST write).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "Other doors to the executor's empty projection (order step 3): REST single + batch go through answerDatasetQuery, which already refuses; MCP query_dataset and the chat tool query_dataset go through dataset-tools.ts, both fixed here. No other caller passes a caller selection to AnalyticsService.queryDataset. carrier: none", "Wording: the tools now answer 'body.selection.measures ...' (the REST sentence, by the parity rule) although a tool input has no body; rewording means changing both doors together. carrier: none, noted in Acceptance notes" ] }