Skip to content

feat(mcp-server): per-user/business/tool rate limiting (MCP Prompt 18)#4022

Open
gilgardosh wants to merge 2 commits into
claude/mcp-prompt-17-error-taxonomyfrom
claude/mcp-prompt-18-rate-limiting
Open

feat(mcp-server): per-user/business/tool rate limiting (MCP Prompt 18)#4022
gilgardosh wants to merge 2 commits into
claude/mcp-prompt-17-error-taxonomyfrom
claude/mcp-prompt-18-rate-limiting

Conversation

@gilgardosh

Copy link
Copy Markdown
Collaborator

Summary

Implements Prompt 18 – Rate limiting middleware (see
docs/mcp/spec.md §11.4). Adds an in-memory
limiter enforced before any upstream call.

Stacked PR: targets claude/mcp-prompt-17-error-taxonomy (Prompt 17, #4021).
Review/merge Prompts 09→17 first; this diff shows only the Prompt 18 changes.

Changes

  • src/rate-limit/limiter.tsRateLimiter, a fixed-window counter with an injectable clock (deterministic tests). rateLimitKey scopes buckets to userId + sorted business scope + tool (spec §11.4). parseRateLimitConfig reads MCP_RATE_LIMIT_CONFIG (JSON) with safe defaults ({ windowMs: 60000, max: 60 }).
  • src/rate-limit/default-limiter.ts — lazily-memoized, env-backed process limiter.
  • src/tools/execute.ts — an optional limiter is enforced after authorization and before the handler (so it's ahead of the upstream call). Over-limit returns a RATE_LIMIT_ERROR via the unified taxonomy with retryAfterMs.
  • src/mcp/handler.ts — the dispatch path passes the process limiter (built lazily, only when a real tool runs).
  • Tests — burst-then-deny, per-key isolation, window reset, denied-doesn't-consume, config parsing, and end-to-end limiting through executeRegisteredTool (incl. business-scope keying). 202 tests total.

Validation

  • yarn workspace @accounter/mcp-server test → 202 passed
  • yarn workspace @accounter/mcp-server lint / typecheck / build → pass

Notes / open decisions

  • In-memory, per-process (spec allows this for phase 1). For a multi-instance deployment this should move to a shared store (Redis); the RateLimiter interface is small enough to swap.
  • limiter is optional on executeRegisteredTool so the existing tool unit tests run without touching env; the live dispatch path always supplies it.
  • Fixed-window (not token-bucket) for simplicity and determinism; conservative default (60/min per user·scope·tool) to be tuned after observing production traffic.

🤖 Generated with Claude Code


Generated by Claude Code

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Prompt 18 rate limiting to the MCP server by introducing an in-memory fixed-window limiter, wiring it into tool execution/dispatch, and documenting + testing the behavior end-to-end.

Changes:

  • Introduced RateLimiter + rateLimitKey and env-driven config parsing (MCP_RATE_LIMIT_CONFIG).
  • Enforced rate limiting in executeRegisteredTool (after authz, before handler/upstream call) and plumbed the process limiter from the MCP request handler.
  • Added unit + integration-style tests for limiter behavior and tool-execution limiting; documented the new behavior in the package README.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/mcp-server/src/tools/execute.ts Adds optional limiter enforcement and RATE_LIMIT_ERROR mapping before handler execution.
packages/mcp-server/src/tools/tests/rate-limit.execute.test.ts Validates end-to-end limiting through executeRegisteredTool, including scope-based keying.
packages/mcp-server/src/rate-limit/limiter.ts Introduces in-memory fixed-window limiter, keying helper, and env config parsing.
packages/mcp-server/src/rate-limit/default-limiter.ts Adds lazy, memoized process-wide limiter configured from env.
packages/mcp-server/src/rate-limit/tests/limiter.test.ts Covers limiter behavior (burst, isolation, reset, denied-doesn’t-consume) and config parsing.
packages/mcp-server/src/mcp/handler.ts Passes the default process limiter into tool execution on tools/call.
packages/mcp-server/README.md Documents rate limiting behavior, error code, and configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/mcp-server/src/rate-limit/limiter.ts Outdated
Comment thread packages/mcp-server/src/tools/execute.ts Outdated
Comment thread packages/mcp-server/src/tools/execute.ts Outdated
Comment thread packages/mcp-server/src/rate-limit/limiter.ts Outdated
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-17-error-taxonomy branch from b704dcf to 827b2f6 Compare July 22, 2026 12:00
claude added 2 commits July 22, 2026 12:01
Add an in-memory rate limiter enforced before upstream calls (docs/mcp/spec.md
§11.4).

- src/rate-limit/limiter.ts: fixed-window RateLimiter with an injectable clock;
  rateLimitKey scopes buckets to userId + sorted business scope + tool;
  parseRateLimitConfig reads MCP_RATE_LIMIT_CONFIG with safe defaults
  ({windowMs:60000,max:60})
- src/rate-limit/default-limiter.ts: memoized env-backed limiter
- executeRegisteredTool enforces an optional limiter after policy, before the
  handler; over-limit returns RATE_LIMIT_ERROR with retryAfterMs (via the
  unified taxonomy); dispatch wires the process limiter
- tests: burst/deny, per-key isolation, window reset, denied-not-consuming,
  config parsing, and end-to-end limiting through executeRegisteredTool

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
- parseRateLimitConfig: floor `max` before the positivity check so a fractional
  value in (0, 1) (e.g. {"max":0.5}) falls back to the default instead of
  becoming max: 0, which would rate-limit every request.
- Introduce a structural `RateLimiterLike` interface and depend on it from the
  tool executor, so an alternative limiter (e.g. shared/Redis-backed) can be
  swapped in without changing signatures.
- Prune expired buckets periodically so the in-memory bucket map cannot grow
  without bound in a long-lived process with many distinct keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-18-rate-limiting branch from a2dc24d to f545baf Compare July 22, 2026 12:03
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 22, 2026 12:03 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 22, 2026 12:03 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants