Register the MCP durable quota policy as a function, not a config-referenced Resource (#1809)#1821
Conversation
…erenced Resource (#1809) The quota hook was configured by `mcp.<profile>.quota.resource` pointing at an exported Resource, whose inherited CRUD then surfaced on every transport (update_/delete_ MCP tools + REST/SSE/WS/GraphQL/MQTT) — a permitted client could reset its own counter. The docs example worked around it by turning six exportTypes flags off, which made the safe path the easy-to-forget one. Replace it with a registration function: `server.setMcpQuotaHandler(fn)`. The policy is a plain function (never an exposed Resource), enabled by registering it (no config). checkDurableQuota invokes the registered handler; no handler => allowed (opt-in), throw => fail-closed deny (unchanged). The handler receives `profile` so one handler can gate operations vs application. - Remove the `mcp.<profile>.quota.resource`/`.method` config params. - Wire `server.setMcpQuotaHandler` next to `server.registerOperation`. - Migrate the mcp-quota fixture off `tables.QuotaCounter`: an exported tool (Answerer) + an internal (non-@export) counter table + a registered handler, so nothing exposes the counter. Integration test asserts the counter is not REST-reachable. Supersedes the docs#576 six-`false` example; docs follow-up separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
There was a problem hiding this comment.
Code Review
This pull request refactors the durable MCP quota policy mechanism by replacing the configuration-based resource lookup with a functional registration approach via server.setMcpQuotaHandler. This change simplifies the quota logic, removes obsolete configuration parameters, and ensures that the quota counter table remains internal and not exposed over REST. The review feedback correctly identifies that the setMcpQuotaHandler signatures in the Server interface and serverUtilities.ts should be updated to accept undefined to match the underlying implementation, allowing handlers to be cleared or unregistered.
|
Reviewed; no blockers found. The previously-open finding (candidate quota handler live on the shared singleton during deploy-validation load) is resolved by commit 8da8768: registrations now no-op during validation instead of snapshot/restore, closing the race entirely and generalizing the same protection to server.registerOperation. |
…earing (review) Codex review: - P1: the quota handler is a process-wide singleton, so a candidate component's top-level server.setMcpQuotaHandler(...) during deploy pre-flight validation would outlive the throwaway load and alter live enforcement on a failed deploy. Snapshot the handler before the validation load and restore it in the finally (added getMcpQuotaHandler()). - P2: the Server interface rejected undefined though the setter supports clearing; widen the public type to McpQuotaHandler | undefined. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
…UD tools (#1809) Codify the security property the redesign delivers (and that /verify checked by hand): the counter table is internal, so GET /QuotaCounter 404s and tools/list carries no QuotaCounter update_/delete_ tools a client could call to reset its quota. Tightened the counter-not-exposed assertion from !=200 to ==404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
…ion isolation (#1809) The P1 snapshot/restore was inline in the deploy op and only its get/set primitive was covered. Extract it into withMcpQuotaHandlerPreserved(fn) — operations.js wraps the throwaway validation load in it — and unit-test the isolation directly: a candidate that registers a different handler, one that clears it, and a load that throws all leave the live worker's handler intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
…view) /code-review: assign server.setMcpQuotaHandler directly instead of a redundant wrapper (also keeps the impl param type in sync with the Server interface's McpQuotaHandler | undefined). Document that withMcpQuotaHandlerPreserved restores unconditionally, so a legitimate interleaving registration would be reverted — a narrow window, and the lesser evil vs leaking a candidate policy live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
…egistrations (#1809) /code-review #3: the deploy pre-flight snapshot/restore only isolated the quota handler; a candidate's server.registerOperation during the throwaway validation load still mutated the process-wide operation map AND announced to the main thread, leaking onto the live worker on a failed deploy. Replace the quota-specific withMcpQuotaHandlerPreserved with a general guard (deployValidationState.ts): server.registerOperation and server.setMcpQuotaHandler both no-op while a validation load is in flight (validation only needs to surface load-time errors, not register anything). operations.js wraps the validation load in runWithDeployValidationGuard. Skipping is cleaner than snapshot/restore here — it also suppresses the cross-thread operation announce, which a local restore can't undo. Depth-counted; the narrow interleaving caveat is documented. Tests: registerOperation + setMcpQuotaHandler are skipped during validation and resume after (incl. after a thrown load), in serverUtilities.test.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
kriszyp
left a comment
There was a problem hiding this comment.
Love it!
As KrAIs said "Kyle didn't just build [the option we were discussing] — he improved on it"
Conflict in components/operations.js: main (#1809/#1821) wrapped the throwaway deploy-validation load in runWithDeployValidationGuard so process-wide server.* registrations (registerOperation, setMcpQuotaHandler) are no-op'd during validation. This branch had already refactored that inline load into the loadValidateComponent() helper. Resolved by keeping the helper and porting main's guard into it — which also extends the guard to the two-phase staging validation load (loadValidateComponent serves both the one-shot and two-phase paths), not just the one-shot path main touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes #1809. Supersedes the interim docs fix in HarperFast/documentation#576.
Problem
The durable MCP quota hook (#1610) was configured by
mcp.<profile>.quota.resourcepointing at an exported Resource. Because Resources are public-by-default, that Resource's inherited CRUD surfaced on every transport (update_/delete_MCP tools + REST/SSE/WS/GraphQL/MQTT), so a client subject to the quota could reset its own counter. The docs example papered over it by turning sixexportTypesflags off — making the safe path the easy-to-forget one, for the one built-in whose entire purpose is server-side enforcement. Per @kriszyp's review on documentation#576, the fix belongs in the API, not the docs.Change
Configure the quota policy with a registration function instead:
profileis in the info so one handler covers both profiles.server.setMcpQuotaHandlernext toserver.registerOperation(serverUtilities.ts), declared on theServerinterface. Worker-local like the tool dispatch that consults it, so no cross-thread announce (matches the old per-worker registry resolution).mcp.<profile>.quota.resource/.methodconfig params (only 5.2-alpha shipped them; no deprecated fallback).mcp-quotafixture offtables.QuotaCounter: an exported tool (Answerer) + an internal (non-@export) counter table + a registered handler — nothing exposes the counter. The integration test now asserts the counter is not REST-reachable.Where to look
server.setMcpQuotaHandlerwiring (serverUtilities.ts) — availability timing (set at server init, before components load) and the worker-local rationale.profile. Two components both registering means last-wins — acceptable for a single policy, worth a glance.mcp-quotaend-to-end couldn't run locally (this machine lacks the127.0.0.xloopback aliases the test framework needs); unit tests (quota.test.js) pass locally and the fixture mirrors the provencustom-resourcesextends Resource+mcpToolspattern.Generated by an LLM (Claude Opus 4.8). Cross-model review (Codex + Gemini) in progress.
Docs: companion docs PR HarperFast/documentation#576 rewrites the
reference/mcp/configuration.mddurable-quota section toserver.setMcpQuotaHandler(removes themcp.<profile>.quota.*config); merges together.