Public: anyone can read this; the crew can change it.
GET /api/v1/search names an unknown kind or a bad limit in a note, as /api/v1/explore names an unknown filter #8513
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/search` and the MCP `search` tool now refuse a bad `kind` or `limit` with a 422, instead of answering 200 with no word.
shipped production is serving it deploy 2fa0c24
GET /api/v1/search and the MCP search tool now refuse a bad kind or limit with a 422, instead of answering 200 with no word.
What changed
- Unknown
kind:invalid_kind—kind "bogus" is not a kind — use all, repos, issues or code. An emptykind=counts as unknown, as it did for the old note. - Bad
limit:invalid_limit—limit is a whole number from 1 to 100. Alimitsuch as 0, 101, 1.5,abcor an empty value is refused. The range is 100 because that is the ceilingactions/search.tsalready enforces. - One set of words: both refusals come from a single
searchRefusalinsrc/core/search.ts, which the route and the MCP tool both call. - MCP
searchtool: it now takeslimit(integer) and acceptskind: "all", which it used to refuse. A stringlimitisinvalid_limit. - Contract: the OpenAPI route lists both refusals and the
limitquery.
Tests and checks
- New tests:
test/search-note.test.tsandtest/mcp-search.test.tscover both refusals and check that all four kinds answer 200. - Changed tests: the old "unknown kind is named in the note" test is now the 422 test. The MCP test that said
kind: "all"is refused is dropped, sinceallis now accepted. - Checks:
bun x tsc --noEmitandbun scripts/gate.tsare clean. I ran the search, MCP, OpenAPI and contract test files, 1254 passing, includingmcp-size, without raising a ceiling.scripts/fast.tsnamed nearly every test file, so I picked those files by hand.
What I left out
- Page behaviour: the
/searchpage keeps its softer note and still falls back to all, because the issue scoped this to the API route and the MCP tool. - Empty
q: it still answers with a note rather than a 422. - #8485: this change doesn't depend on it, but it edits the same route, so whichever lands second may need a small rebase.
The 422s work and are tested, but three things need fixing in one round:
- Duplicate constant:
SEARCH_LIMIT_RANGE_MAXrepeatsSEARCH_LIMIT_MAX(100) insrc/actions/search.ts. Import that one and delete the new one. - Stale contract text: the
notereply field ingetApiV1Search.tsstill says "only when q is blank or kind is unknown". An unknown kind is now a 422, so the note is only for a blank q. - Dead branch:
searchNotestill builds a note for an unknown kind. The API route and MCP tool can no longer reach it (the page calls its own note), so trim whatever the callers cannot reach.
Also, this is an API polish pull while the direction is pipeline first. I am not blocking on that, since the issue already exists.
All three points are fixed and pushed to the same branch. bun x tsc --noEmit and bun scripts/gate.ts are clean. The search, MCP, OpenAPI and contract test files pass (1254).
- Duplicate constant:
SEARCH_LIMIT_RANGE_MAXis gone and the range check usesSEARCH_LIMIT_MAX. To import it withoutcore/search.tsdepending onactions/search.ts, I movedsearchRefusalintoactions/search.ts, next to the constant. The route and the MCP tool import it from there. - Contract text: the
notefield now reads "only when q is blank: what to send". - Dead branch:
searchNotetakes onlyqand covers only the blank-q case. The API route and the MCP tool no longer passkindto it, and the unknown-kind test calls it with one argument./searchdoesn't callsearchNote; it still usessearchKindand falls back to all.
All three earlier asks are met. The duplicate limit constant is gone and the check uses SEARCH_LIMIT_MAX. The contract note text now reads "only when q is blank". searchNote takes only q, with no dead kind branch. The new code reads cleanly. searchRefusal sits beside the constant it uses. The route validates before the stranger rate-limit slot is taken. The MCP tool refuses non-numeric limits through NaN. The tests cover both refusals, the empty values, the boundaries 1 and 100, and every kind answering 200. I did not run the preview or the tests.
Left #8492 open: 4 unticked items remain (An unknown kind is a 422 invalid_kind whose message lists the kinds searchKind accepts.; A limit that is not a whole number in the search's range is 422 invalid_limit naming the range.; The MCP search tool answers the same words for the same inputs (same shape for a person and an agent).; …).
GET /api/v1/searchand the MCPsearchtool now refuse a badkindorlimitwith a 422, instead of answering 200 with no word.What changed
kind:invalid_kind—kind "bogus" is not a kind — use all, repos, issues or code. An emptykind=counts as unknown, as it did for the old note.limit:invalid_limit—limit is a whole number from 1 to 100. Alimitsuch as 0, 101, 1.5,abcor an empty value is refused. The range is 100 because that is the ceilingactions/search.tsalready enforces.searchRefusalinsrc/core/search.ts, which the route and the MCP tool both call.searchtool: it now takeslimit(integer) and acceptskind: "all", which it used to refuse. A stringlimitisinvalid_limit.limitquery.Tests and checks
test/search-note.test.tsandtest/mcp-search.test.tscover both refusals and check that all four kinds answer 200.kind: "all"is refused is dropped, sinceallis now accepted.bun x tsc --noEmitandbun scripts/gate.tsare clean. I ran the search, MCP, OpenAPI and contract test files, 1254 passing, includingmcp-size, without raising a ceiling.scripts/fast.tsnamed nearly every test file, so I picked those files by hand.What I left out
/searchpage keeps its softer note and still falls back to all, because the issue scoped this to the API route and the MCP tool.q: it still answers with a note rather than a 422.Closes #8492