agentic-community / agentic-community/mcp-gateway-registry
Support MCP server-initiated notifications end-to-end through the gateway proxy
- 主要言語
- Python
- スター
- 911
- フォーク
- 234
- 平均マージ
- 1日 11時間
- マージ済み PR(30日)
- 62
説明
## Summary
Support MCP server-initiated notifications (e.g. `notifications/tools/list_changed`, `notifications/resources/updated`, progress and logging notifications) end-to-end through the gateway proxy.
Today these are **not supported**, and the reason is structural rather than a missing handler: since the tools/list filtering hop was introduced (#1026), every MCP POST is routed through the auth-server `/mcp-proxy` endpoint, which **fully buffers** the upstream response before returning it to the client. Server-to-client notifications that depend on an open, incrementally-flushed stream therefore cannot reach the client while the stream is live.
## Current behavior (why it is not implicitly supported)
1. **Client -> server notifications** (e.g. `notifications/initialized`) flow through, but only because the method is explicitly allow-listed in the scope check (`registry/services/scope_service.py`). Other `notifications/*` methods are not allow-listed and would be denied by authorization.
2. **Server -> client notifications** are blocked by response buffering on the inner proxy hop:
- `auth_server/server.py` opens `client.stream(...)` and then immediately calls `_read_bounded(...)`, which accumulates all chunks into a list and only returns once the upstream stream closes.
- The result is returned as a single non-streaming `Response`. Even though the content-type (`text/event-stream`) is preserved, the body is delivered all at once after the stream ends, not incrementally.
- nginx `proxy_buffering off` on the SSE transport block does not help, because the buffering is on the auth-server -> upstream hop downstream of nginx.
- If an upstream holds the stream open to push notifications, the proxy blocks until `MCP_PROXY_TIMEOUT` and then returns everything at once (or times out).
## Proposed work
1. **Stream-preserving proxy path**: for methods other than `tools/list` (which needs the full body for filtering), relay the upstream response to the client as a `StreamingResponse` that forwards `aiter_bytes()` chunk-by-chunk, preserving SSE framing and flush timing. Keep the bounded-buffer + filter path only for `tools/list`.
2. **Scope allow-list**: broaden the `notifications/*` allow-list so notification methods are not denied by authorization.
3. **Long-lived SSE / streamable-HTTP channel**: handle the server-push GET channel for streamable-http transport (the current proxy is POST-oriented per #1026), so server-initiated notifications have a delivery path.
4. **Tests**: add coverage asserting notifications are delivered incrementally (not buffered until stream close) and that `tools/list` filtering still works.
## Acceptance criteria
- A server-initiated `notifications/tools/list_changed` (and `resources/updated`) reaches a connected MCP client through the gateway while the stream is live.
- `tools/list` response filtering (#1026) continues to work unchanged.
- The DoS body-size cap is preserved for the buffered (filtered) path.
- Deploy modes (docker-compose, Helm, ECS) all covered.
## References
- Buffering proxy hop: `auth_server/server.py` (`_read_bounded`, `mcp_proxy`), introduced with tools/list filtering in #1026.
- Scope allow-list: `registry/services/scope_service.py` (`notifications/initialized`).
- nginx SSE transport block: `registry/core/nginx_service.py`.
コントリビューションガイド
評価
この issue はまだ評価されていません。