Skip to content

feat: adopt per-user MCP server rate limiting - #7191

Merged
Lang-Akshay merged 5 commits into
mainfrom
codex/6230-rate-limiter-per-server
Oct 16, 2026
Merged

Lang-Akshay merged 5 commits into
mainfrom
codex/6230-rate-limiter-per-server

Conversation

@vishu-bh

@vishu-bh vishu-bh commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Pull Request

🔗 Related Issue

Closes #6230


📝 Summary

Adopt cpex-rate-limiter>=0.1.12 to enforce independent user quotas for each upstream MCP catalog registration.
When Alice exhausts MCP-A, she can still call MCP-B, and Bob can still call MCP-A.
All tools from MCP-A share Alice's quota across gateway replicas through Redis.

  • Adopt the October 15 release with the prompt-hook correction from fix(rate-limiter): exclude prompts from per-server tool quotas cpex-plugins#203 and regenerate the lockfile with a targeted upgrade.
  • Expose by_user_per_server: null while preserving disabled mode and the existing global limits.
  • Document opt-in configuration, the global by_user ceiling, unsupported user_scope, and the required replica-wide package rollout.
  • Reset gateway-less contexts to unknown and align Rust fallback identity with Python invocation.
  • Add live acceptance coverage for regular users, explicit teams, wildcard bindings, multiple tools, two replicas, and both transport modes.
  • Verify REST GET/POST and MCP prompt retrieval, preserve global prompt limits, and exclude prompts from catalog quotas.
  • Run acceptance coverage in both static and binding integration jobs; retain global-only and combined-limit regression cases.

The limiter algorithm comes from the upstream package. No unrelated dependency versions change.


📏 Reviewability

  • This PR has one clear purpose
  • The linked issue is not labeled triage
  • Unrelated bugs or improvements are tracked in separate issues/PRs
  • Tests are included with the code they validate
  • If AI-assisted, I understand and can explain the generated changes

🏷️ Type of Change

  • Bug fix
  • Feature / Enhancement
  • Documentation
  • Refactor
  • Chore (deps, CI, tooling)
  • Other (describe below)

🧪 Verification

Latest targeted checks use the published 0.1.12 wheel. Full-suite, coverage, and production rows retain earlier validation results.

Check Command Status
Dependency adoption uv lock --upgrade-package cpex-rate-limiter Locks 0.1.12; no unrelated dependency versions change
Lint suite make ruff interrogate pylint Passed
Unit tests make test Earlier full-suite run: 23,987 passed, 938 skipped, two expected failures. Published 0.1.12 focused suite: 802 passed, one skipped
Service and transport tests .venv/bin/pytest tests/unit/mcpgateway/services/test_tool_service_tenant_id.py tests/unit/mcpgateway/services/test_tool_service.py tests/unit/mcpgateway/transports/ --no-cov -q Passed; one existing transport skip
Existing limiter integration .venv/bin/pytest tests/integration/test_rate_limiter.py tests/integration/test_rate_limiter_redis_url_from_yaml.py --with-integration --no-cov -q 56 passed against the published 0.1.12 wheel
New gateway acceptance .venv/bin/pytest tests/integration/test_rate_limiter_per_server.py -v --with-integration --no-cov 24 passed, 12 expected prompt skips on the tool-binding leg; published 0.1.12 wheel
Coverage ≥ 80% make coverage diff-cover 98% total coverage; 100% changed-line coverage; nested pytest reports one failure
Formatting and hygiene make pre-commit Passed; the new acceptance file also passes an explicit file-scoped run
Package verification make verify Passed
Secret detection make detect-secrets-scan Passed
Production Rust image make docker-prod-rust IMAGE_BASE=cf6230-gateway IMAGE_TAG=validation Passed
Production MCP and RBAC E2E make test-e2e against an isolated production container 104 passed, 15 failed, seven skipped, three setup errors
Existing live limiter suites .venv/bin/pytest tests/integration/test_rate_limiter_dynamic_behavior.py tests/integration/test_rate_limiter_multi_tenant.py --with-integration --no-cov -v Three passed, four failed with stateful sessions and Redis caching
Security scan make bandit Reports findings outside the changed service; the changed service passes a file-scoped scan

The acceptance suite exercises all three issue criteria through live gateways and shared Redis.
Alice and Bob are regular users with developer roles and explicit team scopes.
Wildcard runtime bindings share a catalog quota across two tools. Inaccessible tools and virtual servers are rejected.
Prompt cases cover REST GET/POST and MCP across virtual servers and replicas. Tool bindings do not activate prompt hooks.
Each transport run starts two gateway processes with a shared catalog database and an isolated Redis prefix.
An hourly window prevents minute-boundary races, and every invocation initializes a fresh MCP session.


✅ Checklist

  • Code formatted (Ruff and pre-commit)
  • Tests added/updated for changes
  • Documentation updated (if applicable)
  • No secrets or credentials committed

Formatting verification uses Ruff and pre-commit. Separate Black and isort commands are not run.


📓 Notes (optional)

The License Check job exposes an inherited metadata issue from main.
PR #6760 adds completion-test-server without a license declaration.
This PR changes root pyproject.toml, which triggers the workflow and its repository-wide Python manifest scan.
Commit 13fb22067 adds license = "Apache-2.0" to the fixture manifest.
Validation: the Python license scanner evaluates all five manifests and reports zero findings; git diff --check passes.
Failure log: https://github.com/IBM/mcp-context-forge/actions/runs/38144605649/job/114486278804?pr=7191

Required pre-merge validation remains incomplete. No waiver is approved.
The published 0.1.12 wheel resolves the prompt regression and passes the focused validation above.
The full pre-merge gate has not been rerun after dependency adoption. Earlier gate limitations follow.

  • The coverage run fails test_safe_error_detail_never_returns_exception_text because the fallback includes a correlation reference suffix.
    The full test gate passes this unchanged test.
  • Production E2E failures include resource-template event-loop errors, token-revocation assertions, and detached-tool error text.
  • Existing live limiter failures include the old sequential mode expectation, burst transport errors, and HTTP 404 session calls.
  • Repository Bandit findings occur outside the changed service.
  • The exact make docker-nuke docker-prod-rust testing-up RUST_MCP_MODE= setup is not executed.
    docker-nuke deletes all Docker resources, including unrelated resources.
    An isolated production image and container replace that setup locally. The full Compose topology remains unverified.

Resolve these failures or document waivers before merge readiness. The human author must complete the AI-assisted reviewability confirmation.

Adopt cpex-rate-limiter 0.1.11 and expose opt-in configuration.

Normalize gateway-less plugin context identity.

Add Redis acceptance coverage for both transport modes and gateway replicas.

Closes #6230

Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
@vishu-bh
vishu-bh force-pushed the codex/6230-rate-limiter-per-server branch from 169d9af to 3add8a6 Compare October 11, 2026 13:34
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
@vishu-bh
vishu-bh marked this pull request as ready for review October 11, 2026 13:47
@vishu-bh vishu-bh self-assigned this Oct 11, 2026
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>

@msureshkumar88 msureshkumar88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The tool-quota implementation uses the existing gateway UUID and shared Redis. This is a backend change; no UI change or Alembic migration is needed. I reviewed it against #6230.

blocking | plugins/config.yaml:315 | Enabling by_user_per_server breaks ordinary prompt retrieval | Keep the new quota specific to tool invocations, preserve existing prompt limits, and add REST/MCP prompt regression tests.

The shipped limiter registers both tool_pre_invoke and prompt_pre_fetch. Version 0.1.11 requires server_id in both hooks when by_user_per_server is configured. PromptService.get_prompt() uses an optional virtual-server ID at mcpgateway/services/prompt_service.py:2017. Ordinary prompt requests omit this ID.

Live reproduction with limiter 0.1.11: the same created prompt returns HTTP 200 with by_user=2/h and by_user_per_server=null. It returns HTTP 503 with by_user=null and by_user_per_server=2/h. The error is RATE_LIMIT_CONTEXT_MISSING. Virtual-server prompt calls also use a different quota identity from tool calls: virtual-server UUID versus upstream catalog UUID.

Fix the hook behavior in the upstream limiter and adopt the corrected package here. Do not require clients to supply a quota identity. Preserve existing prompt protection.

functionally-impacting | tests/integration/test_rate_limiter_per_server.py:75 | The new acceptance suite uses unrestricted admins and static enforcement only | Add regular-user, explicit-team, and runtime-binding cases, or document a testing waiver.

Alice and Bob both use is_admin=true with teams=null. The suite creates no distinct tenant scopes. The new workflow step runs only in the static enforcement leg. Existing binding tests exercise by_user, not by_user_per_server. Add a team-scoped wildcard binding across two tools from one catalog. Verify tenant isolation and rejection of inaccessible resources.

Validation: six focused unit tests and all four new live acceptance cases pass. Coverage confirms the changed executable gateway line runs. An additional live prompt comparison confirms the regression.

The full pre-merge gate and production Rust transport were not rerun. Please fix the prompt regression before merge; the existing tool-quota design can remain.

@msureshkumar88 msureshkumar88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The tool-quota implementation uses the existing gateway UUID and shared Redis. The current head, 13fb2206756e8e80e8a5aad595761c26b9c35082, still contains the two findings from the October 13 review. CI passes, but the requested changes remain absent.

blocking | plugins/config.yaml:315 | Enabling by_user_per_server breaks ordinary prompt retrieval | Adopt corrected limiter behavior that applies per-server quotas to tool invocations, preserves existing prompt limits, and adds REST/MCP prompt regression tests.

The configured limiter registers `prompt_pre_fetch` and `tool_pre_invoke`. Version 0.1.11 requires `server_id` for both hooks when `by_user_per_server` is configured. Ordinary prompt retrieval omits this optional ID. The prior live reproduction returns HTTP 503 with `RATE_LIMIT_CONTEXT_MISSING` when per-server quotas are enabled. Virtual-server prompt calls also use a virtual-server UUID, while tool calls use the upstream catalog UUID. Fix this behavior in the upstream limiter and adopt the corrected package. Clients must not supply a quota identity.

functionally-impacting | tests/integration/test_rate_limiter_per_server.py:75 | Acceptance coverage still uses unrestricted admins and static enforcement only | Add regular-user, explicit-team, and runtime-binding cases, or document a testing waiver.

Alice and Bob use `is_admin=true` with `teams=null`. The suite creates no distinct tenant scopes. The workflow runs this suite only in the static enforcement leg. Add a team-scoped wildcard binding across two tools from one catalog. Verify tenant isolation and rejection of inaccessible resources.

Validation: Rechecked the current PR head, dependency selection, configuration, acceptance coverage, and workflow against the existing review. The head remains unchanged. Full tests and live gateway reproductions were not rerun during this follow-up review.

Require cpex-rate-limiter 0.1.12 for corrected prompt hooks.
Cover scoped users, tenant isolation, wildcard bindings, and REST/MCP prompts.
Run acceptance coverage through both enforcement paths.

PyPI publication remains pending. Regenerate uv.lock after publication.
Validation uses the merged upstream correction: 802 unit tests,
56 existing integration tests, and 24 live gateway cases pass.
Static hooks pass with UV_NO_SYNC=1 while publication is pending.

Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
Resolve the published prompt-hook correction from PyPI.
Update only cpex-rate-limiter and its lockfile metadata.

Validation: 802 unit tests, 56 existing integration tests,
and 24 live gateway cases pass against the published wheel.
Normal pre-commit hooks and uv lock --check pass.

Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>

@msureshkumar88 msureshkumar88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The per-user catalog quota fix addresses both previous review findings. The 0.1.12 dependency corrects prompt-hook behavior and preserves existing global limits. Acceptance coverage includes regular users, explicit teams, wildcard bindings, multiple tools, both transport modes, and two replicas.

No actionable code defects remain in this review. Independent verification passes five focused service cases, 24 live acceptance cases, and 55 existing limiter cases. Twelve prompt cases skip for tool-only bindings. One existing YAML Redis case skips because its Redis endpoint is unavailable. All 53 current CI checks pass; five other checks skip.

This approval covers the reviewed code at f945923. Required pre-merge validation remains outstanding as documented in the PR description. Resolve the reported coverage and production E2E failures, and complete the required gate or document approved waivers before merge. This review does not grant a validation waiver.

Not rerun independently: the full pre-merge gate and production Rust transport.

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM ✅

@Lang-Akshay
Lang-Akshay added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 12fa6ab Oct 16, 2026
58 checks passed
@Lang-Akshay
Lang-Akshay deleted the codex/6230-rate-limiter-per-server branch October 16, 2026 09:34
@msureshkumar88 msureshkumar88 mentioned this pull request Oct 19, 2026
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.

Scope per-user rate limits by MCP server

3 participants