feat(mcp): full UI filter parity for search and alerts
The flat schema only supported minimums, so it could express a fraction of what
the web filter can. Now mirrors ServerFilter feature-for-feature:
- min/max on every numeric range, not just minimums
- per-disk vs total size modes; per-disk requires EVERY drive of that type to
fit the range, and an empty drive list passes trivially (matching the SQL's
array_filter/array_length identity)
- disk_mode and/or, where "or" only counts disk types the caller actually
constrained — otherwise an unconstrained type satisfies it and the filter
becomes a no-op
- exact cpu_models, socket count, multiple locations and datacenters
Matching semantics ported from src/lib/api/frontend/filter.ts, the reference
implementation the UI runs, so an alert created over MCP behaves identically to
one created in the browser.
Also adds min/max_drive_count for total drives across types — "three disks" in
ordinary speech means that, and no per-type filter finds a 1 NVMe + 2 SATA
machine.
MCP speaks plain GB/EUR throughout; the log2 and 500 GB encodings stay hidden in
filter.ts. Permissive open bounds are used for unconstrained dimensions rather
than defaultFilter's opinionated starting values, whose hddInternalSize floor of
4 (2000 GB) would silently exclude servers the caller never asked to exclude.
feat(mcp): public MCP server with authenticated alert management
POST /mcp speaks stateless Streamable HTTP JSON-RPC. Read tools work with no
credentials; presenting a valid MCP access token additionally exposes alert
management, so tools/list is conditional on the Authorization header.
- search_auctions / get_auction serve from the KV snapshot, so MCP traffic
reads zero D1 rows. cloud_availability proxies the worker.
- list_alerts / create_alert / delete_alert are gated on a Better Auth MCP
session. Migration 0017 adds the OAuth provider tables (camelCase, unlike the
rest of the schema: MCPOptions has no field-mapping escape hatch and nothing
in the app queries them).
- Alert tools take the same flat schema as search_auctions and build the
ServerFilter server-side, in defaultFilter's key order — idx_price_alert_
user_id_filter is UNIQUE on the serialised string, so key order is
load-bearing.
Encodings were taken from MATCH_ALERTS_SQL rather than the UI, and verified
against production: RAM is log2(GB), disk sizes are units of 500 GB, and
price_alert.price is GROSS whole euros with vat_rate as a percentage — not
cents and not a decimal rate, as the spec had assumed.
Also fixes baseURL, which was pinned to the production origin. Better Auth's
isAuthPath() rejects a mismatched origin, so every /api/auth/* route 404'd on
localhost and would have on any preview deployment.
feat(mcp): full UI filter parity for search and alerts
The flat schema only supported minimums, so it could express a fraction of what
the web filter can. Now mirrors ServerFilter feature-for-feature:
- min/max on every numeric range, not just minimums
- per-disk vs total size modes; per-disk requires EVERY drive of that type to
fit the range, and an empty drive list passes trivially (matching the SQL's
array_filter/array_length identity)
- disk_mode and/or, where "or" only counts disk types the caller actually
constrained — otherwise an unconstrained type satisfies it and the filter
becomes a no-op
- exact cpu_models, socket count, multiple locations and datacenters
Matching semantics ported from src/lib/api/frontend/filter.ts, the reference
implementation the UI runs, so an alert created over MCP behaves identically to
one created in the browser.
Also adds min/max_drive_count for total drives across types — "three disks" in
ordinary speech means that, and no per-type filter finds a 1 NVMe + 2 SATA
machine.
MCP speaks plain GB/EUR throughout; the log2 and 500 GB encodings stay hidden in
filter.ts. Permissive open bounds are used for unconstrained dimensions rather
than defaultFilter's opinionated starting values, whose hddInternalSize floor of
4 (2000 GB) would silently exclude servers the caller never asked to exclude.