MCP: time_backoff prevents required Streamable HTTP handshake after rate limit threshold
- Dominant language
- PHP
- Stars
- 3.4k
- Forks
- 1.2k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 433
Description
### Shopware Version
6.7.11.1
### Affected area / extension
Platform (experimental MCP server at `/api/_mcp`)
### Actual behaviour
After the MCP rate limiter has crossed its first `time_backoff` threshold, a standard Streamable HTTP MCP client can no longer complete the required handshake with direct integration-header authentication.
After waiting until the reported backoff had fully elapsed, the following controlled sequence was observed:
1. `initialize` returned HTTP 200 and a valid MCP session ID.
2. The immediately following `notifications/initialized` request for the same session returned HTTP 429.
3. The JSON-RPC error was:
```json
{
"code": -32029,
"message": "MCP endpoint throttled for 59 seconds."
}
```
The controlled test stopped at that point; `tools/list` was not retried.
Authentication itself is valid:
- Direct `sw-access-key` / `sw-secret-access-key` headers are accepted.
- `initialize` succeeds with HTTP 200.
- The same integration authenticates successfully against the Admin API using `client_credentials` (HTTP 200).
A multi-minute quiet period does not restore the ability to perform the complete handshake. Once the backoff threshold has been crossed, the first accepted request starts the timer again, so the next mandatory protocol request is throttled immediately.
### Expected behaviour
A correctly authenticated MCP client must be able to execute the required initialization sequence without an artificial delay between protocol messages:
1. `initialize`
2. `notifications/initialized`
3. `tools/list`
Rate limiting should protect the endpoint without making a standards-compliant MCP handshake impossible. For example, active-session follow-up messages could be exempted, the limiter could apply only to session creation or invalid authentication, or the post-backoff burst capacity could cover the required handshake messages.
### How to reproduce
1. Run Shopware 6.7.11.1 with the `MCP_SERVER` feature enabled.
2. Create an integration with valid ACL permissions and an MCP allowlist.
3. Authenticate to `POST /api/_mcp` using:
- `sw-access-key: `
- `sw-secret-access-key: `
4. Send enough MCP endpoint requests for the integration to cross the first configured MCP backoff threshold.
5. Wait until the reported backoff has elapsed.
6. Send one valid `initialize` request:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "mcp-rate-limit-reproduction",
"version": "1.0"
}
}
}
```
7. Read the returned `Mcp-Session-Id`.
8. Immediately send the required notification with the same authentication headers and session ID:
```json
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}
```
9. Observe HTTP 429 with `MCP endpoint throttled for 59 seconds.`
### Technical analysis
The behaviour matches the current 6.7.11.1 implementation:
- MCP limiter configuration: `time_backoff`, reset after one hour, thresholds of 60 requests / 60 seconds and 120 requests / 120 seconds:
https://github.com/shopware/shopware/blob/v6.7.11.1/src/Core/Framework/Resources/config/packages/shopware.yaml#L355-L363
- Every MCP protocol request is passed through the MCP limiter:
https://github.com/shopware/shopware/blob/v6.7.11.1/src/Core/Framework/Mcp/Controller/McpServerController.php#L317-L327
- After a backoff interval, exactly one request is accepted; that accepted request increments the attempt count and resets the timer, so the immediately following handshake request is rejected:
https://github.com/shopware/shopware/blob/v6.7.11.1/src/Core/Framework/RateLimiter/Policy/TimeBackoffLimiter.php#L49-L77
- Shopware's limiter test documents the same one-request-after-wait behaviour:
https://github.com/shopware/shopware/blob/v6.7.11.1/tests/integration/Core/Framework/RateLimiter/Policy/TimeBackoffLimiterTest.php#L109-L143
This appears to be an interaction bug between the endpoint-wide backoff policy and the multi-request MCP Streamable HTTP handshake.
---
## Validated against mcp/sdk 0.8.1 + symfony/mcp-bundle 0.13.0
Checked 2026-09-16 against the [#19963](https://github.com/shopware/shopware/pull/19963) branch, which vendors `mcp/sdk` v0.8.1 and `symfony/mcp-bundle` v0.13.0. Both are the latest published releases, so nothing newer is pending. Line numbers refer to that branch.
**The structural bug still stands; the numbers in the body do not.**
Confirmed unchanged on the #19963 branch:
- `McpServerController:91` calls `$this->rateLimiter->enforceForAdminApi($request)` for every JSON-RPC message, with no method-level exemption.
- `McpRateLimiter` (`src/Core/Framework/Mcp/RateLimit/McpRateLimiter.php`) has no notion of `initialize` or `notifications/initialized`; it keys on the OAuth access token id and falls back to the client IP.
- The policy is still `time_backoff`, so the described interaction (one accepted request after the wait restarts the timer, throttling the next mandatory handshake message) is intact.
Stale facts to correct:
- **Limits.** The body quotes 60 requests / 60 seconds and 120 / 120 seconds from 6.7.11.1. Current `shopware.yaml:375-397` has `mcp_admin_api` at 300 / 60 seconds and 1000 / 10 minutes, and `mcp_store_api` at 120 / 60 seconds and 600 / 10 minutes. Raised, so the bug is harder to reach, not fixed.
- **Feature flag.** Repro step 1 says "with the `MCP_SERVER` feature enabled". That flag was removed in #18463 and MCP is always on. The step should just be "on a 6.7 install".
- **Permalinks.** All four links are pinned to `v6.7.11.1` and no longer show the current code.
**Interaction with #19969.** The 2026-07-28 revision has no `initialize` plus `notifications/initialized` pair, so the exact three-message sequence this issue is about does not exist there. Whatever fix is chosen should either target the handshake-era leg explicitly or be expressed per era, otherwise it will read as dead code after the era adoption.
Contributor guide
Assessment
This issue has not been assessed yet.