-
Notifications
You must be signed in to change notification settings - Fork 65
design: add MCP client proxy design doc #674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # MCP Client Proxy Design | ||
|
|
||
| ## Problem | ||
|
|
||
| Users of MCP Gateway have no built-in way to inspect, test, or interact with the tools and prompts exposed by the gateway. To test a tool call or browse available prompts, they must use external tools like MCP Inspector, which requires manually entering the gateway URL and runs outside the platform. There's no integrated experience in OpenShift. | ||
|
|
||
| ## Summary | ||
|
|
||
| The MCP client proxy lives in the developer-portal-controller, a shared backend that both the OpenShift console plugin and RHDH already depend on. The proxy is needed because: | ||
|
|
||
| - Most MCP servers do not enable CORS — while it can be configured, it is not enabled by default and most servers do not support it. Browsers cannot call them directly without it. MCP Inspector solves the same problem with a Node.js backend. | ||
| - A cluster can have multiple MCP Gateways. The ConsolePlugin CR's `spec.proxy` is static and triggers console pod restarts on changes, so it cannot handle dynamic gateway discovery. | ||
|
|
||
| The proxy discovers gateways from MCPGatewayExtension CRs, resolves their endpoints, and makes HTTP requests to them on behalf of the frontend. Users supply their own auth credentials in the UI. | ||
|
|
||
| Dev preview scope: tools (list, call), prompts (list, get). | ||
|
|
||
| ## Goals | ||
|
|
||
| - MCP Inspector-like experience for tools (list, call) and prompts (list, get) through the gateway | ||
| - User-supplied auth token, with capability filtering applied by the gateway | ||
|
|
||
| ## Non-Goals | ||
|
|
||
| - SSE notifications (tool list changes, server-initiated events) | ||
| - OAuth/OIDC flow (automatic auth discovery and login) | ||
|
|
||
| ## Design | ||
|
|
||
| ### Gateway discovery | ||
|
|
||
| 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 derived endpoint uses `http://` by default. When the targeted Gateway listener uses HTTPS protocol, the scheme is `https://` instead. The proxy handles both — Go's `http.Client` supports HTTPS natively. If the gateway uses a custom CA certificate, the proxy will need the CA configured at deploy time. | ||
|
R-Lawton marked this conversation as resolved.
|
||
|
|
||
| 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. | ||
|
|
||
| ### Proxy API | ||
|
|
||
| #### GET /gateways | ||
|
|
||
| Lists all discovered MCPGatewayExtensions and their resolved endpoints. | ||
|
|
||
| ```json | ||
| { | ||
| "gateways": [ | ||
| { | ||
| "name": "mcp-gateway-extension", | ||
| "namespace": "team-a", | ||
| "publicHost": "mcp.team-a.example.com", | ||
| "ready": true | ||
| }, | ||
| { | ||
| "name": "mcp-gateway-extension", | ||
| "namespace": "team-b", | ||
| "publicHost": "mcp.team-b.example.com", | ||
| "ready": true | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| The `ready` field reflects the MCPGatewayExtension's `Ready` status condition. The `publicHost` is derived from the Gateway listener hostname or `spec.publicHost`. | ||
|
|
||
| #### POST /gateways/\<namespace\>/\<name\>/mcp | ||
|
|
||
| Proxies an MCP JSON-RPC request to the specified gateway. Step by step: | ||
|
|
||
| 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 | ||
|
R-Lawton marked this conversation as resolved.
|
||
|
|
||
| 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://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. | ||
|
R-Lawton marked this conversation as resolved.
|
||
|
|
||
| ### Auth model | ||
|
|
||
| - Users enter their auth token in the plugin UI | ||
| - The frontend includes it as the `Authorization` header on requests to the proxy | ||
| - The proxy copies the header onto the request it sends to the gateway | ||
| - AuthPolicy at the gateway validates the token | ||
| - The proxy never inspects, stores, or validates the token | ||
|
|
||
| If no AuthPolicy is configured on the gateway, requests pass through without auth. | ||
|
|
||
|
R-Lawton marked this conversation as resolved.
|
||
| ### Session management | ||
|
|
||
| 1. Frontend sends `initialize` (no `Mcp-Session-Id` yet) | ||
| 2. Gateway creates a session and returns a `Mcp-Session-Id` JWT in the response header | ||
| 3. Proxy passes this header back to the frontend | ||
| 4. Frontend stores it in memory (not persisted to localStorage or cookies) | ||
| 5. All subsequent requests include `Mcp-Session-Id` as a header | ||
| 6. Proxy passes it through to the gateway | ||
|
|
||
| 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. | ||
|
R-Lawton marked this conversation as resolved.
|
||
|
|
||
| ## Security Considerations | ||
|
|
||
| - **Auth tokens are passed through, not stored**: the proxy copies the `Authorization` header from the frontend request to the gateway request. It does not log, cache, or persist tokens. | ||
| - **Session JWTs in browser memory**: `Mcp-Session-Id` is held in frontend state, not persisted. Lost on page refresh. | ||
| - **All auth validation happens at the gateway**: the proxy does not make auth decisions. | ||
| - **RBAC for gateway discovery**: the proxy's ServiceAccount needs `get`, `list`, `watch` on MCPGatewayExtension CRs and `get` on Gateway resources. | ||
| - **No direct broker access for MCP requests**: all MCP protocol requests go through the gateway, preserving the full security model. | ||
|
|
||
| ## Future Considerations | ||
|
|
||
| - **OAuth/OIDC flow**: automatic auth discovery via RFC 9728 and Authorization Code flow, eliminating manual token entry | ||
| - **SSE notifications**: subscribe to tool list changes for live updates | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.