tools/list returns empty array silently when AWS MCP Server throttles concurrent proxy initialization
- Dominant language
- Python
- Stars
- 352
- Forks
- 63
- Avg merge
- 12h 9m
- Merged PRs (30d)
- 19
Description
### Describe the bug (Security related? please follow to report them to AWS Security directly.)
## Summary
When running multiple `mcp-proxy-for-aws` instances concurrently against `https://aws-mcp.us-east-1.api.aws/mcp` (one per AWS profile/region), some instances receive HTTP 200 with `{"tools":[]}` for `tools/list`, indistinguishable from a legitimately empty server. The proxy silently passes this through to the MCP client, which assumes the server has no tools and never retries. Other instances in the same batch correctly return the full tool list, and a few visibly fail with `429 Too Many Requests`.
The empty-array case appears to be a silent degradation under upstream throttling. Because the proxy does not surface this as an error, the user has no signal that a retry is warranted.
## Environment
- `mcp-proxy-for-aws` version: 1.4.2
- Python: 3.10 (from uv-managed environment)
- MCP client: Claude Desktop
- OS: macOS
- AWS auth: SSO via shared `sso-session`, 9 profiles, role `AdministratorAccess`
- Endpoint: `https://aws-mcp.us-east-1.api.aws/mcp`
## Configuration
9 MCP server entries in client config, each identical except for `AWS_PROFILE` and `--metadata AWS_REGION=...`:
```json
{
"mcpServers": {
"aws-acct-1": {
"command": "uvx",
"args": ["mcp-proxy-for-aws@latest",
"https://aws-mcp.us-east-1.api.aws/mcp",
"--metadata", "AWS_REGION=us-east-1"],
"env": {"AWS_PROFILE": "aws-acct-1"}
},
"aws-acct-2": { "...": "AWS_REGION=eu-west-1, AWS_PROFILE=aws-acct-2" }
}
}
```
## Steps to reproduce
1. Configure 8+ `mcp-proxy-for-aws` entries pointing at the same AWS MCP endpoint, with different `AWS_PROFILE` values (valid SSO session, all profiles independently verified with `aws sts get-caller-identity`).
2. Start the MCP client cold. All proxy processes initialize simultaneously.
3. Observe that a subset (in my case 3-6 of 9) correctly receive the full tool list; the rest receive `{"tools":[]}`.
## Expected
Either:
- (a) The upstream returns an explicit `429` for throttled `tools/list` calls, and the proxy propagates it (or retries with backoff), so the client knows the empty state is transient; or
- (b) The proxy treats `tools:[]` on initial `tools/list` as suspicious and retries with backoff before declaring success.
## Actual
The proxy receives HTTP 200 with `{"tools":[]}` and forwards it verbatim. The client sees a successfully-initialized server with zero tools. No retry. The proxy process is later killed by the client (`Server transport closed unexpectedly`) and not restarted.
In the same log file, when actual tool calls are later attempted on other connected proxies, the upstream does return `429 Too Many Requests` — confirming the service has rate-limiting in play. The asymmetry between explicit-429 on `tools/call` and silent-empty on `tools/list` is the core problem.
## Log excerpts
**Affected instance (empty tools, no error):**
```
[info] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
[info] Message from server: {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}
[info] Server transport closed unexpectedly
```
**Different instance, later in same session (explicit 429):**
```
httpx.HTTPStatusError: Client error '429 Too Many Requests' for url 'https://aws-mcp.us-east-1.api.aws/mcp'
at fastmcp/client/client.py line 668, _disconnect
at mcp/client/streamable_http.py line 358, _handle_post_request
```
## Workaround
Manually staggered restart with `pkill -f "AWS_REGION="; sleep 15-30` per server, allowing each proxy to complete `tools/list` before the next starts. Inconvenient but reliable.
## Suggested fixes
1. **Proxy side:** On `tools/list` returning `{"tools":[]}` during initial handshake, log a WARNING with the response headers (in case the service is conveying retry hints), and optionally retry once with backoff before passing through. The `--retries` flag (default 0) does not currently appear to cover this case.
2. **Service side:** When throttling `tools/list`, return an explicit `429` (with `Retry-After`) rather than HTTP 200 + empty array. The current behavior is observationally indistinguishable from "this account has no tools," which is a legitimate but very different state.
3. **Docs:** Note the concurrent-initialization quirk in the README so users with many profiles know to stagger startup.
Happy to provide additional debug-level logs if useful.
### Expected Behavior
When concurrent `mcp-proxy-for-aws` instances initialize, all of them should either:
(a) successfully return the full tool list from `tools/list`, or
(b) propagate an explicit error (e.g., HTTP `429`, possibly with `Retry-After`) so the MCP client knows the empty-tools state is transient and can retry.
A silent `{"tools":[]}` response indistinguishable from "this server has no tools" should not occur as a side-effect of upstream throttling.
### Current Behavior
When running multiple `mcp-proxy-for-aws` instances concurrently against `https://aws-mcp.us-east-1.api.aws/mcp` (one per AWS profile/region), some instances receive HTTP 200 with `{"tools":[]}` for `tools/list`, indistinguishable from a legitimately empty server. The proxy silently passes this through to the MCP client, which assumes the server has no tools and never retries.
In the same session, when actual tool calls are later attempted on other connected proxies, the upstream does return `429 Too Many Requests` — confirming the service is rate-limiting. The asymmetry between explicit-429 on `tools/call` and silent-empty on `tools/list` is the core problem: there is no signal to the user that a retry is warranted.
**Affected instance (empty tools, no error):**
```
[info] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
[info] Message from server: {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}
[info] Server transport closed unexpectedly
```
**Different instance, later in same session (explicit 429):**
```
httpx.HTTPStatusError: Client error '429 Too Many Requests' for url 'https://aws-mcp.us-east-1.api.aws/mcp'
at fastmcp/client/client.py line 668, _disconnect
at mcp/client/streamable_http.py line 358, _handle_post_request
```
The proxy process is later killed by the client (`Server transport closed unexpectedly`) and not restarted.
### Reproduction Steps
1. Configure 8+ `mcp-proxy-for-aws` entries in your MCP client (e.g., Claude Desktop) pointing at the same AWS MCP endpoint, each with a different `AWS_PROFILE`:
```json
{
"mcpServers": {
"aws-acct-1": {
"command": "uvx",
"args": ["mcp-proxy-for-aws@latest",
"https://aws-mcp.us-east-1.api.aws/mcp",
"--metadata", "AWS_REGION=us-east-1"],
"env": {"AWS_PROFILE": "aws-acct-1"}
},
"aws-acct-2": {
"command": "uvx",
"args": ["mcp-proxy-for-aws@latest",
"https://aws-mcp.us-east-1.api.aws/mcp",
"--metadata", "AWS_REGION=eu-west-1"],
"env": {"AWS_PROFILE": "aws-acct-2"}
}
}
}
```
2. Verify all profiles work independently: `aws sts get-caller-identity --profile aws-acct-N` for each.
3. Start the MCP client cold (e.g., fully quit and relaunch Claude Desktop). All proxy processes initialize simultaneously.
4. Observe MCP server logs. In my testing, 3-6 of 9 proxies receive the full tool list; the remaining receive `{"tools":[]}` and are subsequently terminated.
**Workaround:** Manually staggered restart with `pkill -f "AWS_REGION="; sleep 15-30` per server, allowing each proxy to complete `tools/list` before the next starts.
### Possible Solution
Two complementary fixes:
1. **Proxy side:** On `tools/list` returning `{"tools":[]}` during initial handshake, log a WARNING with the upstream response headers (in case the service is conveying retry hints), and optionally retry once with backoff before passing through. The `--retries` flag (default 0) does not currently appear to cover this case.
2. **Service side (upstream AWS MCP):** When throttling `tools/list`, return an explicit `429` (ideally with `Retry-After`) rather than HTTP 200 + empty array. The current behavior is observationally indistinguishable from "this account has no tools," which is a legitimate but very different state. The proxy already propagates `429` correctly on `tools/call` — applying the same on `tools/list` would solve this end-to-end.
3. **Docs:** Note the concurrent-initialization quirk in the README so users with many profiles know to stagger startup.
### Additional Information/Context
- All 9 AWS profiles use the same `sso-session` and `AdministratorAccess` permission set. Each was independently verified with `aws sts get-caller-identity` returning the expected account ID.
- The split between "working" and "empty tools" instances varies across cold starts of the MCP client — i.e., it's not a stable per-profile property. This strongly suggests the cause is a transient upstream condition (rate limiting) and not anything account- or region-specific.
- Once the client is in this state, the affected proxy processes do not recover on their own. Manually killing the proxy process with `pkill -f "AWS_REGION="` (one at a time, with 15-30s spacing) reliably restores them.
- Initial SSO token refresh (`aws sso login`) before relaunching the MCP client improved success rate but did not fully resolve the issue with 8+ concurrent proxies.
Happy to provide additional DEBUG-level logs or test patches if helpful.
### Operating System
macOS
### Release Version
1.4.2
### Other information
Related: issue #22630 in anthropics/claude-code describes a similar "connected but no tools" symptom against `mcp-proxy-for-aws`, though with a different error path (`-32602: Invalid request parameters` instead of silent empty array). May share root cause with this report — worth cross-referencing during triage.
Link: https://github.com/anthropics/claude-code/issues/22630
### Search verification
- [x] I have searched existing issues and verified this is not a duplicate
### Related issues
None in this repository. A related but distinct symptom was reported in anthropics/claude-code#22630 (different error path, possibly different root cause).
Contributor guide
Research direction
Start by tracing the proxy’s tools/list request path and how the --retries flag is applied; the report names no source file or test. Reproduce with 8+ concurrent profiles and compare the empty 200 response with the observed 429 logs. Done means the transient throttling case is surfaced or retried rather than silently presented as a successful empty tool list.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, python
- Domain
- api, backend, cloud
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100