design: add MCP client proxy design doc - #2
Conversation
Defines the proxy architecture for MCP Inspector-like functionality in the console plugin. The proxy lives in the developer-portal-controller and handles gateway discovery, auth passthrough, session management, and multi-gateway support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: R-Lawton <rlawton@redhat.com>
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a design document for an MCP client proxy that discovers ChangesMCP client proxy
Estimated code review effort: 1 (Trivial) | ~5 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add accurate JSON-RPC request and response examples for all MCP operations (initialize, notifications/initialized, tools/list, tools/call, prompts/list, prompts/get) matching the MCP Gateway's actual protocol version (2025-03-26) and response formats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: R-Lawton <rlawton@redhat.com>
Replace verbose MCP request/response examples with a link to the MCP specification. Simplify goals section. Remove redundant MCP operations table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: R-Lawton <rlawton@redhat.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/design/mcp-client-proxy.md`:
- Around line 123-125: The MCP client/session handling flow should automatically
re-initialize when a session-bearing request receives a 404. Update the
frontend/client logic to clear the in-memory Mcp-Session-Id, send a fresh
InitializeRequest without that header, replace the stored ID with the new
session ID, and retry within a bounded limit while preserving the existing error
response when retries are exhausted.
- Line 100: Update the MCP specification link in the proxy documentation to use
the canonical URL https://modelcontextprotocol.io/specification/2025-03-26/,
while leaving the MCP Gateway docs link unchanged.
- Around line 32-39: The endpoint derivation documentation must use the
controller-managed internal host rather than constructing a host from the
Gateway name and class. Document that the proxy selects http:// or https://
based on the referenced listener protocol, while preserving spec.privateHost as
a verbatim endpoint override.
- Around line 72-102: Update the MCP protocol and proxy-flow documentation to
clarify that the implementation supports a gateway-specific JSON-only subset,
including its handling of notifications/initialized and responses.
Alternatively, document and require forwarding the frontend Accept header,
upstream status, Content-Type, and SSE response payloads, including 202 no-body
responses for notifications. Ensure the stated operations and response contract
match the implemented behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fe6a51be-bbbe-42da-ba5d-dc2d47ed1668
📒 Files selected for processing (1)
docs/design/mcp-client-proxy.md
| The proxy watches MCPGatewayExtension CRs across the cluster using the k8s API. For each CR, it resolves the gateway endpoint: | ||
|
|
||
| 1. Read `spec.targetRef` on the MCPGatewayExtension — this points to a Gateway resource with a `name`, `namespace`, and `sectionName` (listener) | ||
| 2. Read the referenced Gateway resource to get the `gatewayClassName` and the listener's port | ||
| 3. If `spec.privateHost` is set on the MCPGatewayExtension, use that as the endpoint | ||
| 4. Otherwise, derive the endpoint: `<gateway-name>-<gatewayClassName>.<gateway-namespace>.svc.cluster.local:<port>` | ||
|
|
||
| The proxy maintains a map of MCPGatewayExtension (namespace + name) → gateway endpoint. When CRs are created, updated, or deleted, the map is updated. No console restarts are needed. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'privateHost|gatewayClassName|cluster\.local|listener.*protocol' .Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 579
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== docs/design/mcp-client-proxy.md (lines 1-140) ==\n'
cat -n docs/design/mcp-client-proxy.md | sed -n '1,140p'
printf '\n== search for gateway Service/endpoint references ==\n'
rg -n 'gatewayClassName|privateHost|svc\.cluster\.local|listener.*protocol|HTTPS|HTTP|Streamable|MCPGatewayExtension' docs . --glob '!**/node_modules/**'Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 12582
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== docs/design/mcp-client-proxy.md (lines 1-220) ==\n'
cat -n docs/design/mcp-client-proxy.md | sed -n '1,220p'
printf '\n== related docs and references ==\n'
rg -n 'MCPGatewayExtension|privateHost|gatewayClassName|Streamable HTTP|SSE|session|404' docs . --glob '!**/node_modules/**' --glob '!**/.git/**'Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 20175
🌐 Web query:
MCPGatewayExtension privateHost gatewayClassName service name scheme listener protocol kuadrant docs
💡 Result:
In the Kuadrant MCP Gateway documentation, the MCPGatewayExtension resource is used to enable MCP protocol support on a Kubernetes Gateway [1][2]. Key configuration details for your inquiry include: MCPGatewayExtension privateHost The privateHost field in the MCPGatewayExtension specification is used to override the internal host address utilized for hair-pinning requests back through the gateway [1][3]. By default, this value is computed as -istio-..svc.cluster.local:, where the port is derived from the targeted Gateway listener [1][4][3]. If the targeted listener uses the HTTPS protocol, the default address is prefixed with https://; otherwise, it defaults to http:// [1][5][3]. You can set this field explicitly to use a different host, port, or scheme (e.g., https://my-gw:443), and the provided value is honored verbatim [1][6][3]. gatewayClassName and Service Mapping The MCPGatewayExtension targets a specific Gateway resource via the targetRef field [1][7]. The controller reads the Gateway's gatewayClassName and configuration to manage the broker/router deployment [6][4]. While gatewayClassName is a property of the Gateway itself rather than the MCPGatewayExtension, it dictates the underlying Gateway API provider (such as Istio or OpenShift) that the MCP Gateway must interact with [6][3]. The controller also automatically manages a service (typically named mcp-gateway) in the same namespace as the MCPGatewayExtension, which acts as the broker for MCP traffic [4][2]. Listener Protocol and Scheme The MCPGatewayExtension requires a sectionName that identifies a specific listener on the target Gateway [1][6][7]. The controller inspects this listener to determine the port and protocol (HTTP/HTTPS) [1][6]. For successful operation, the listener's protocol influences both the public address configuration and the internal hair-pinning scheme [8][4][5]. If your environment requires specific handling for HTTPS listeners—such as using an HTTP listener on a different port for internal hair-pinning—you can explicitly configure the privateHost field to point to that specific address [5][3].
Citations:
- 1: https://docs.kuadrant.io/dev/mcp-gateway/docs/reference/mcpgatewayextension/
- 2: https://docs.kuadrant.io/dev/mcp-gateway/docs/guides/how-to-install-and-configure/
- 3: https://docs.redhat.com/en/documentation/red_hat_connectivity_link/1.4/pdf/installing_the_mcp_gateway/Red_Hat_Connectivity_Link-1.4-Installing_the_MCP_gateway-en-US.pdf
- 4: https://docs.kuadrant.io/latest/mcp-gateway/docs/guides/how-to-install-and-configure/
- 5: Router hairpin uses wrong scheme/port for HTTPS Gateway listeners mcp-gateway#917
- 6: https://docs.kuadrant.io/dev/mcp-gateway/docs/guides/migrating-mcpgatewayextension/
- 7: https://docs.kuadrant.io/dev/mcp-gateway/docs/guides/register-mcp-servers/
- 8: https://docs.kuadrant.io/dev/mcp-gateway/docs/guides/configure-mcp-gateway-listener-and-router/
Document the actual internal endpoint shape and scheme. <gateway-name>-<gatewayClassName>... is not the published default here; use the controller-managed internal host instead, and derive http:// vs https:// from the listener protocol. Keep spec.privateHost as a verbatim override.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design/mcp-client-proxy.md` around lines 32 - 39, The endpoint
derivation documentation must use the controller-managed internal host rather
than constructing a host from the Gateway name and class. Document that the
proxy selects http:// or https:// based on the referenced listener protocol,
while preserving spec.privateHost as a verbatim endpoint override.
| 1. The frontend sends a POST with: | ||
| - Body: a JSON-RPC payload (e.g., `{"jsonrpc":"2.0","method":"tools/list","id":1}`) | ||
| - `Authorization` header: the user's auth token (if provided) | ||
| - `Mcp-Session-Id` header: the MCP session ID (if an active session exists) | ||
| - `Content-Type: application/json` | ||
|
|
||
| 2. The proxy: | ||
| - Parses the namespace and name from the URL | ||
| - Looks up the gateway endpoint from its map | ||
| - Creates a new HTTP POST request to `<gateway-endpoint>/mcp` | ||
| - Copies the body from the frontend request | ||
| - Copies the `Authorization` header as-is | ||
| - Copies the `Mcp-Session-Id` header if present | ||
| - Sets `Content-Type: application/json` | ||
| - Sends this request to the gateway | ||
|
|
||
| 3. The gateway processes the request through its full stack: | ||
| - AuthPolicy validates the `Authorization` token (if configured) | ||
| - ext_proc router parses the JSON-RPC method, manages sessions, routes tool calls to the correct upstream MCP server | ||
| - Broker handles initialize, tools/list, prompts/list, prompts/get | ||
|
|
||
| 4. The proxy: | ||
| - Reads the gateway's response | ||
| - Copies the `Mcp-Session-Id` response header back to the frontend | ||
| - Returns the JSON response body to the frontend | ||
|
|
||
| ### MCP protocol | ||
|
|
||
| The proxy forwards standard MCP Streamable HTTP requests. For the full protocol specification, see the [MCP specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/) and the [MCP Gateway docs](https://docs.kuadrant.io). | ||
|
|
||
| The operations in scope are: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, `prompts/list`, and `prompts/get`. All are JSON-RPC over POST, all return JSON responses. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
curl -fsSL https://modelcontextprotocol.io/specification/2025-03-26/basic/transports |
rg -n 'Accept|202 Accepted|text/event-stream|Mcp-Session-Id'Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 50392
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== docs/design/mcp-client-proxy.md (around lines 72-102) =="
sed -n '72,102p' docs/design/mcp-client-proxy.md | cat -n
echo
echo "== spec excerpts for Streamable HTTP =="
tmp=$(mktemp)
curl -fsSL https://modelcontextprotocol.io/specification/2025-03-26/basic/transports > "$tmp"
# Pull only the headings and the lines around the relevant transport requirements.
rg -n -C 2 'Accept: application/json, text/event-stream|202 Accepted|text/event-stream|notifications/initialized|Streamable HTTP|Listening for Messages from the Server|Sending Messages to the Server' "$tmp" | head -n 200
rm -f "$tmp"Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 50392
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== docs/design/mcp-client-proxy.md lines 72-102 =="
sed -n '72,102p' docs/design/mcp-client-proxy.md | cat -n
echo
echo "== file size =="
wc -l docs/design/mcp-client-proxy.mdRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 2141
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from urllib.request import urlopen
import re
url = "https://modelcontextprotocol.io/specification/2025-03-26/basic/transports"
html = urlopen(url, timeout=20).read().decode("utf-8", "replace")
# crude text extraction: strip scripts/styles and tags
html = re.sub(r'(?is)<script.*?</script>', ' ', html)
html = re.sub(r'(?is)<style.*?</style>', ' ', html)
text = re.sub(r'(?s)<[^>]+>', '\n', html)
text = re.sub(r'\n{3,}', '\n\n', text)
needles = [
"Accept: application/json, text/event-stream",
"202 Accepted",
"text/event-stream",
"notifications/initialized",
"Listening for Messages from the Server",
"Sending Messages to the Server",
"Streamable HTTP",
]
for needle in needles:
print(f"\n== {needle} ==")
for m in re.finditer(re.escape(needle), text):
start = max(0, m.start() - 300)
end = min(len(text), m.end() + 500)
snippet = text[start:end].replace("\n", " ")
print(snippet)
break
PYRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 2767
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== mentions in docs/design/mcp-client-proxy.md =="
rg -n -i 'sse|event-stream|202 accepted|Accept:|Streamable HTTP|JSON response|notification|notifications/initialized|Mcp-Session-Id' docs/design/mcp-client-proxy.md
echo
echo "== nearby context around any hits =="
for n in $(rg -n -i 'sse|event-stream|202 accepted|Accept:|Streamable HTTP|JSON response|notification|notifications/initialized|Mcp-Session-Id' docs/design/mcp-client-proxy.md | cut -d: -f1 | head -n 10); do
start=$((n>8 ? n-8 : 1))
end=$((n+8))
echo "--- lines $start-$end ---"
sed -n "${start},${end}p" docs/design/mcp-client-proxy.md
doneRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 11219
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== mentions in docs/design/mcp-client-proxy.md =="
rg -n -i 'sse|event-stream|202 accepted|Accept:|Streamable HTTP|JSON response|notification|notifications/initialized|Mcp-Session-Id' docs/design/mcp-client-proxy.md
echo
echo "== broader repo mentions of Streamable HTTP / SSE =="
rg -n -i 'Streamable HTTP|text/event-stream|202 Accepted|notifications/initialized|Accept: application/json, text/event-stream' docsRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 2898
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for log in /tmp/coderabbit-shell-logs/shell-output-0ldTsA /tmp/coderabbit-shell-logs/shell-output-65FbWO /tmp/coderabbit-shell-logs/shell-output-B3Q9Zo /tmp/coderabbit-shell-logs/shell-output-eYvaLz /tmp/coderabbit-shell-logs/shell-output-orUmAm /tmp/coderabbit-shell-logs/shell-output-ochVIm; do
[ -f "$log" ] || continue
echo "== $log =="
rg -n -C 2 'Accept: application/json, text/event-stream|202 Accepted|text/event-stream|notifications/initialized|Streamable HTTP|request-bearing POST|notification-only POST' "$log" | head -n 80 || true
echo
doneRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 50392
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from urllib.request import urlopen
import ssl, re
url = "https://modelcontextprotocol.io/specification/2025-03-26/basic/transports"
ctx = ssl._create_unverified_context()
html = urlopen(url, context=ctx, timeout=20).read().decode("utf-8", "replace")
# Strip scripts/styles and tags to text
html = re.sub(r'(?is)<script.*?</script>', ' ', html)
html = re.sub(r'(?is)<style.*?</style>', ' ', html)
text = re.sub(r'(?s)<[^>]+>', '\n', html)
text = re.sub(r'\n{3,}', '\n\n', text)
needles = [
"Accept: application/json, text/event-stream",
"202 Accepted",
"text/event-stream",
"notifications/initialized",
"Streamable HTTP",
]
for needle in needles:
print(f"\n== {needle} ==")
m = re.search(re.escape(needle), text)
if not m:
print("NOT FOUND")
continue
start = max(0, m.start() - 250)
end = min(len(text), m.end() + 500)
print(text[start:end].replace("\n", " "))
PYRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 1889
Document the Streamable HTTP response contract. The proxy is described as “standard MCP Streamable HTTP”, but notifications/initialized is notification-only and the protocol allows 202 Accepted/no body plus Accept: application/json, text/event-stream and SSE responses for request-bearing POSTs. Either call out this as a gateway-specific JSON-only subset, or forward Accept, upstream status, Content-Type, and SSE payloads.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design/mcp-client-proxy.md` around lines 72 - 102, Update the MCP
protocol and proxy-flow documentation to clarify that the implementation
supports a gateway-specific JSON-only subset, including its handling of
notifications/initialized and responses. Alternatively, document and require
forwarding the frontend Accept header, upstream status, Content-Type, and SSE
response payloads, including 202 no-body responses for notifications. Ensure the
stated operations and response contract match the implemented behavior.
| Each user gets their own session with each gateway. User A connecting to gateway-team-a has a different session than user B connecting to the same gateway, or user A connecting to gateway-team-b. The proxy doesn't track any of this — it just copies the `Mcp-Session-Id` header from responses and onto subsequent requests. It never decodes, validates, or stores the value. | ||
|
|
||
| If the gateway returns a 404 (session expired or invalid), the proxy passes it back to the frontend. The user must manually reconnect — the proxy does not automatically re-initialize sessions. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
curl -fsSL https://modelcontextprotocol.io/specification/2025-03-26/basic/transports |
rg -n '404 Not Found|new.*InitializeRequest|new session'Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 50392
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== docs/design/mcp-client-proxy.md (around lines 110-135) ==\n'
sed -n '110,135p' docs/design/mcp-client-proxy.md
printf '\n== spec excerpts for 2025-03-26 transports ==\n'
python3 - <<'PY'
import re, urllib.request
url = 'https://modelcontextprotocol.io/specification/2025-03-26/basic/transports'
html = urllib.request.urlopen(url, timeout=30).read().decode('utf-8', 'replace')
for pat in [
r'404[^<\n]{0,200}',
r'new session[^<\n]{0,200}',
r'InitializeRequest[^<\n]{0,200}',
r'expired[^<\n]{0,200}',
r'invalid[^<\n]{0,200}',
]:
print(f'\n-- {pat} --')
seen = set()
for m in re.finditer(pat, html, flags=re.I):
snippet = re.sub(r'\s+', ' ', html[max(0, m.start()-120):m.end()+180])
if snippet in seen:
continue
seen.add(snippet)
print(snippet[:400])
if len(seen) >= 5:
break
PYRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 4719
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re, ssl, urllib.request
url = 'https://modelcontextprotocol.io/specification/2025-03-26/basic/transports'
ctx = ssl._create_unverified_context()
html = urllib.request.urlopen(url, context=ctx, timeout=30).read().decode('utf-8', 'replace')
patterns = [
r'404[^<\n]{0,300}',
r'new session[^<\n]{0,300}',
r'InitializeRequest[^<\n]{0,300}',
r're-initialize[^<\n]{0,300}',
r'reinitiali[sz]e[^<\n]{0,300}',
r'session expired[^<\n]{0,300}',
r'invalid[^<\n]{0,300}',
r'Mcp-Session-Id[^<\n]{0,300}',
]
for pat in patterns:
print(f'\n-- {pat} --')
count = 0
for m in re.finditer(pat, html, flags=re.I):
snippet = re.sub(r'\s+', ' ', html[max(0, m.start()-180):m.end()+260])
print(snippet[:700])
count += 1
if count >= 3:
break
PYRepository: Kuadrant/mcp-gateway-console-plugin
Length of output: 7265
🏁 Script executed:
#!/bin/bash
set -euo pipefail
curl -kfsSL 'https://modelcontextprotocol.io/specification/2025-03-26/basic/transports' |
tr '\r' '\n' |
rg -n -C 4 -i 'Mcp-Session-Id|404|re-?initiali[sz]e|session expired|invalid session|InitializeRequest|new session'Repository: Kuadrant/mcp-gateway-console-plugin
Length of output: 50392
Re-initialise automatically on 404. MCP requires a fresh InitializeRequest without Mcp-Session-Id after a 404 on a session-bearing request, so manual reconnect is non-compliant. The frontend/client should own the automatic session reset and swap the in-memory ID with bounded retry behaviour.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design/mcp-client-proxy.md` around lines 123 - 125, The MCP
client/session handling flow should automatically re-initialize when a
session-bearing request receives a 404. Update the frontend/client logic to
clear the in-memory Mcp-Session-Id, send a fresh InitializeRequest without that
header, replace the stored ID with the new session ID, and retry within a
bounded limit while preserving the existing error response when retries are
exhausted.
Add note about HTTP/HTTPS scheme handling in gateway discovery. Fix MCP spec URL to canonical modelcontextprotocol.io domain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: R-Lawton <rlawton@redhat.com>
|
Moved to Kuadrant/kuadrant-console-plugin#674 |
Summary
Key decisions
🤖 Generated with Claude Code
Summary by CodeRabbit