Repository navigation
feat: adopt per-user MCP server rate limiting - #7191
Conversation
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>
169d9af to
3add8a6
Compare
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
msureshkumar88
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
Pull Request
🔗 Related Issue
Closes #6230
📝 Summary
Adopt
cpex-rate-limiter>=0.1.12to 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.
by_user_per_server: nullwhile preserving disabled mode and the existing global limits.by_userceiling, unsupporteduser_scope, and the required replica-wide package rollout.unknownand align Rust fallback identity with Python invocation.The limiter algorithm comes from the upstream package. No unrelated dependency versions change.
📏 Reviewability
triage🏷️ Type of Change
🧪 Verification
Latest targeted checks use the published 0.1.12 wheel. Full-suite, coverage, and production rows retain earlier validation results.
uv lock --upgrade-package cpex-rate-limitermake ruff interrogate pylintmake test.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.venv/bin/pytest tests/integration/test_rate_limiter.py tests/integration/test_rate_limiter_redis_url_from_yaml.py --with-integration --no-cov -q.venv/bin/pytest tests/integration/test_rate_limiter_per_server.py -v --with-integration --no-covmake coverage diff-covermake pre-commitmake verifymake detect-secrets-scanmake docker-prod-rust IMAGE_BASE=cf6230-gateway IMAGE_TAG=validationmake test-e2eagainst an isolated production container.venv/bin/pytest tests/integration/test_rate_limiter_dynamic_behavior.py tests/integration/test_rate_limiter_multi_tenant.py --with-integration --no-cov -vmake banditThe 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
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-serverwithout a license declaration.This PR changes root
pyproject.toml, which triggers the workflow and its repository-wide Python manifest scan.Commit
13fb22067addslicense = "Apache-2.0"to the fixture manifest.Validation: the Python license scanner evaluates all five manifests and reports zero findings;
git diff --checkpasses.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.
test_safe_error_detail_never_returns_exception_textbecause the fallback includes a correlation reference suffix.The full test gate passes this unchanged test.
sequentialmode expectation, burst transport errors, and HTTP 404 session calls.make docker-nuke docker-prod-rust testing-up RUST_MCP_MODE=setup is not executed.docker-nukedeletes 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.