Skip to content

Commit 89bc99d

Browse files
mathetakenutanix-Hrushikesh
authored andcommitted
mcp: relaxes the tight backendRef count limit on MCPRoute (envoyproxy#1284)
**Description** Previously, MCPRoute corresponded to one generated HTTPRoute where rules contain one rule per backendref in the original MCPRoute. Since HTTPRoute has the tight limit on the number of rules that can be defined in one HTTPRoute, we had to limit the number of backendRefs on MCPRoute. The limit is 16 rules per route, so it is a bit inconvenient given that one might want to define more than 20+ tools from multiple backends though generally it's recommended to have less than 30 tools. This mitigates such limitation by simply generating HTTPRoute per backendRef in MCPRoute, so that we will never hit the hard limit by HTTPRoute when adding more backends. --------- Signed-off-by: Takeshi Yoneda <[email protected]> Signed-off-by: Hrushikesh Patil <[email protected]>
1 parent c041ff1 commit 89bc99d

File tree

13 files changed

+287
-150
lines changed

13 files changed

+287
-150
lines changed

api/v1alpha1/mcp_route.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type MCPRouteSpec struct {
7777
//
7878
// +kubebuilder:validation:Required
7979
// +kubebuilder:validation:MinItems=1
80-
// +kubebuilder:validation:MaxItems=32
80+
// +kubebuilder:validation:MaxItems=256
8181
// +kubebuilder:validation:XValidation:rule="self.all(i, self.exists_one(j, j.name == i.name))", message="all backendRefs names must be unique"
8282
BackendRefs []MCPRouteBackendRef `json:"backendRefs"`
8383

examples/mcp/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,13 @@ Once you have everything running you can start the agent by passing a prompt fil
237237
into the terminal.
238238

239239
```shell
240-
$ uv run --exact -q --env-file <environment file>> agent.py /path/to/prompt.txt
240+
$ uv run --exact -q --env-file .env agent.py /path/to/prompt.txt
241241
```
242242

243243
or
244244

245245
```shell
246-
$ uv run --exact -q --env-file <environment file>> agent.py <<EOF
246+
$ uv run --exact -q --env-file .env agent.py << EOF
247247
> your prompt here
248248
EOF
249249
```

internal/controller/mcp_route.go

Lines changed: 118 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,85 @@ func (c *MCPRouteController) syncMCPRoute(ctx context.Context, mcpRoute *aigv1a1
9393
if err := c.ensureMCPProxyBackend(ctx, mcpRoute); err != nil {
9494
return fmt.Errorf("failed to ensure MCP proxy Backend: %w", err)
9595
}
96-
97-
// Check if the HTTPRoute exists.
98-
httpRouteName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name
9996
c.logger.Info("Syncing MCPRoute", "namespace", mcpRoute.Namespace, "name", mcpRoute.Name)
100-
var httpRoute gwapiv1.HTTPRoute
101-
err := c.client.Get(ctx, client.ObjectKey{Name: httpRouteName, Namespace: mcpRoute.Namespace}, &httpRoute)
102-
existingRoute := err == nil
97+
98+
// First, we create or update the main HTTPRoute that routes to the MCP proxy.
99+
// The main HTTPRoutes will not be "moved" into the MCP Backend listener in the extension server.
100+
mainHTTPRouteName := internalapi.MCPMainHTTPRoutePrefix + mcpRoute.Name
101+
mainHTTPRoute, existing, err := c.getOrNewHTTPRouteRoute(ctx, mcpRoute, mainHTTPRouteName)
102+
if err != nil {
103+
return fmt.Errorf("failed to get or create HTTPRoute: %w", err)
104+
}
105+
if err = c.newMainHTTPRoute(mainHTTPRoute, mcpRoute); err != nil {
106+
return fmt.Errorf("failed to construct a new HTTPRoute: %w", err)
107+
}
108+
109+
// Create or Update the main HTTPRoute.
110+
if err = c.createOrUpdateHTTPRoute(ctx, mainHTTPRoute, existing); err != nil {
111+
return fmt.Errorf("failed to create or update HTTPRoute: %w", err)
112+
}
113+
114+
// Then, build HTTPRoute for each backend in the MCPRoute to avoid the hard limit of 16 Rules per HTTPRoute.
115+
// The route here will be moved to the backend listener in the extension server behind the MCP Proxy.
116+
//
117+
// Each backend will have its own rule that matches the internalapi.MCPBackendHeader set by the MCP proxy.
118+
// This allows the MCP proxy to route requests to the correct backend based on the header.
119+
for i := range mcpRoute.Spec.BackendRefs {
120+
ref := &mcpRoute.Spec.BackendRefs[i]
121+
name := mcpPerBackendRefHTTPRouteName(mcpRoute.Name, ref.Name)
122+
var httpRoute *gwapiv1.HTTPRoute
123+
httpRoute, existing, err = c.getOrNewHTTPRouteRoute(ctx, mcpRoute, name)
124+
if err != nil {
125+
return fmt.Errorf("failed to get or create HTTPRoute: %w", err)
126+
}
127+
if err = c.newPerBackendRefHTTPRoute(ctx, httpRoute, mcpRoute, ref); err != nil {
128+
return fmt.Errorf("failed to construct a new HTTPRoute for backend %s: %w", ref.Name, err)
129+
}
130+
if err = c.createOrUpdateHTTPRoute(ctx, httpRoute, existing); err != nil {
131+
return fmt.Errorf("failed to create or update HTTPRoute for backend %s: %w", ref.Name, err)
132+
}
133+
}
134+
135+
// Reconciles MCPRouteSecurityPolicy and creates/updates its associated envoy gateway resources.
136+
if err = c.syncMCPRouteSecurityPolicy(ctx, mcpRoute, mainHTTPRouteName); err != nil {
137+
return fmt.Errorf("failed to sync MCP route security policy: %w", err)
138+
}
139+
140+
err = c.syncGateways(ctx, mcpRoute)
141+
if err != nil {
142+
return fmt.Errorf("failed to sync gw pods: %w", err)
143+
}
144+
return nil
145+
}
146+
147+
func mcpPerBackendRefHTTPRouteName(mcpRouteName string, backendName gwapiv1.ObjectName) string {
148+
return fmt.Sprintf("%s%s-%s", internalapi.MCPPerBackendRefHTTPRoutePrefix, mcpRouteName, backendName)
149+
}
150+
151+
func (c *MCPRouteController) createOrUpdateHTTPRoute(ctx context.Context, httpRoute *gwapiv1.HTTPRoute, update bool) error {
152+
if update {
153+
c.logger.Info("Updating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
154+
if err := c.client.Update(ctx, httpRoute); err != nil {
155+
return fmt.Errorf("failed to update HTTPRoute: %w", err)
156+
}
157+
} else {
158+
c.logger.Info("Creating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
159+
if err := c.client.Create(ctx, httpRoute); err != nil {
160+
return fmt.Errorf("failed to create HTTPRoute: %w", err)
161+
}
162+
}
163+
return nil
164+
}
165+
166+
func (c *MCPRouteController) getOrNewHTTPRouteRoute(ctx context.Context, mcpRoute *aigv1a1.MCPRoute, routeName string) (*gwapiv1.HTTPRoute, bool, error) {
167+
httpRoute := &gwapiv1.HTTPRoute{}
168+
err := c.client.Get(ctx, client.ObjectKey{Name: routeName, Namespace: mcpRoute.Namespace}, httpRoute)
169+
existing := err == nil
103170
if apierrors.IsNotFound(err) {
104171
// This means that this MCPRoute is a new one.
105-
httpRoute = gwapiv1.HTTPRoute{
172+
httpRoute = &gwapiv1.HTTPRoute{
106173
ObjectMeta: metav1.ObjectMeta{
107-
Name: httpRouteName,
174+
Name: routeName,
108175
Namespace: mcpRoute.Namespace,
109176
Labels: make(map[string]string),
110177
Annotations: make(map[string]string),
@@ -121,44 +188,17 @@ func (c *MCPRouteController) syncMCPRoute(ctx context.Context, mcpRoute *aigv1a1
121188
for k, v := range mcpRoute.Annotations {
122189
httpRoute.Annotations[k] = v
123190
}
124-
if err = ctrlutil.SetControllerReference(mcpRoute, &httpRoute, c.client.Scheme()); err != nil {
125-
panic(fmt.Errorf("BUG: failed to set controller reference for HTTPRoute: %w", err))
191+
if err = ctrlutil.SetControllerReference(mcpRoute, httpRoute, c.client.Scheme()); err != nil {
192+
return nil, false, fmt.Errorf("failed to set controller reference for HTTPRoute: %w", err)
126193
}
127194
} else if err != nil {
128-
return fmt.Errorf("failed to get HTTPRoute: %w", err)
129-
}
130-
131-
// Update the HTTPRoute with the new MCPRoute.
132-
if err = c.newHTTPRoute(ctx, &httpRoute, mcpRoute); err != nil {
133-
return fmt.Errorf("failed to construct a new HTTPRoute: %w", err)
134-
}
135-
136-
if existingRoute {
137-
c.logger.Info("Updating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
138-
if err = c.client.Update(ctx, &httpRoute); err != nil {
139-
return fmt.Errorf("failed to update HTTPRoute: %w", err)
140-
}
141-
} else {
142-
c.logger.Info("Creating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
143-
if err = c.client.Create(ctx, &httpRoute); err != nil {
144-
return fmt.Errorf("failed to create HTTPRoute: %w", err)
145-
}
195+
return nil, false, fmt.Errorf("failed to get HTTPRoute: %w", err)
146196
}
147-
148-
// Reconciles MCPRouteSecurityPolicy and creates/updates its associated envoy gateway resources.
149-
if err = c.syncMCPRouteSecurityPolicy(ctx, mcpRoute, httpRouteName); err != nil {
150-
return fmt.Errorf("failed to sync MCP route security policy: %w", err)
151-
}
152-
153-
err = c.syncGateways(ctx, mcpRoute)
154-
if err != nil {
155-
return fmt.Errorf("failed to sync gw pods: %w", err)
156-
}
157-
return nil
197+
return httpRoute, existing, nil
158198
}
159199

160-
// newHTTPRoute updates the HTTPRoute with the new MCPRoute.
161-
func (c *MCPRouteController) newHTTPRoute(ctx context.Context, dst *gwapiv1.HTTPRoute, mcpRoute *aigv1a1.MCPRoute) error {
200+
// newMainHTTPRoute updates the main HTTPRoute with the MCPRoute.
201+
func (c *MCPRouteController) newMainHTTPRoute(dst *gwapiv1.HTTPRoute, mcpRoute *aigv1a1.MCPRoute) error {
162202
// This routes incoming MCP client requests to the MCP proxy in the ext proc.
163203
servingPath := ptr.Deref(mcpRoute.Spec.Path, defaultMCPPath)
164204
rules := []gwapiv1.HTTPRouteRule{{
@@ -325,24 +365,44 @@ func (c *MCPRouteController) newHTTPRoute(ctx context.Context, dst *gwapiv1.HTTP
325365
rules = append(rules, authServerSuffixRule)
326366
}
327367
}
368+
dst.Spec.Rules = rules
328369

329-
// Build HTTPRouteRules for each backend in the MCPRoute.
330-
// Each backend will have its own rule that matches the internalapi.MCPBackendHeader set by the MCP proxy.
331-
// This allows the MCP proxy to route requests to the correct backend based on the header.
332-
for i := range mcpRoute.Spec.BackendRefs {
333-
ref := &mcpRoute.Spec.BackendRefs[i]
334-
if ns := ref.Namespace; ns != nil && *ns != gwapiv1.Namespace(mcpRoute.Namespace) {
335-
// TODO: do this in a CEL or webhook validation or start supporting cross-namespace references with ReferenceGrant.
336-
return fmt.Errorf("cross-namespace backend reference is not supported: backend %s/%s in MCPRoute %s/%s",
337-
*ns, ref.Name, mcpRoute.Namespace, mcpRoute.Name)
338-
}
339-
mcpBackendToHTTPRouteRule, err := c.mcpBackendRefToHTTPRouteRule(ctx, mcpRoute, ref)
340-
if err != nil {
341-
return fmt.Errorf("failed to convert MCPRouteRule to HTTPRouteRule: %w", err)
342-
}
343-
rules = append(rules, mcpBackendToHTTPRouteRule)
370+
// Initialize labels and annotations maps if they don't exist.
371+
if dst.Labels == nil {
372+
dst.Labels = make(map[string]string)
344373
}
345-
dst.Spec.Rules = rules
374+
if dst.Annotations == nil {
375+
dst.Annotations = make(map[string]string)
376+
}
377+
378+
// Copy labels from MCPRoute to HTTPRoute.
379+
for k, v := range mcpRoute.Labels {
380+
dst.Labels[k] = v
381+
}
382+
383+
// Copy non-controller annotations from MCPRoute to HTTPRoute.
384+
for k, v := range mcpRoute.Annotations {
385+
dst.Annotations[k] = v
386+
}
387+
388+
// Mark this HTTPRoute as generated by the MCP Gateway controller with the hash of the backend refs so that
389+
// this will invoke an extension server update.
390+
dst.Spec.ParentRefs = mcpRoute.Spec.ParentRefs
391+
return nil
392+
}
393+
394+
// newPerBackendRefHTTPRoute creates an HTTPRoute for each backend reference in the MCPRoute.
395+
func (c *MCPRouteController) newPerBackendRefHTTPRoute(ctx context.Context, dst *gwapiv1.HTTPRoute, mcpRoute *aigv1a1.MCPRoute, ref *aigv1a1.MCPRouteBackendRef) error {
396+
if ns := ref.Namespace; ns != nil && *ns != gwapiv1.Namespace(mcpRoute.Namespace) {
397+
// TODO: do this in a CEL or webhook validation or start supporting cross-namespace references with ReferenceGrant.
398+
return fmt.Errorf("cross-namespace backend reference is not supported: backend %s/%s in MCPRoute %s/%s",
399+
*ns, ref.Name, mcpRoute.Namespace, mcpRoute.Name)
400+
}
401+
mcpBackendToHTTPRouteRule, err := c.mcpBackendRefToHTTPRouteRule(ctx, mcpRoute, ref)
402+
if err != nil {
403+
return fmt.Errorf("failed to convert MCPRouteRule to HTTPRouteRule: %w", err)
404+
}
405+
dst.Spec.Rules = []gwapiv1.HTTPRouteRule{mcpBackendToHTTPRouteRule}
346406

347407
// Initialize labels and annotations maps if they don't exist.
348408
if dst.Labels == nil {
@@ -457,7 +517,7 @@ func mcpProxyBackendName(mcpRoute *aigv1a1.MCPRoute) string {
457517
}
458518

459519
func mcpBackendRefFilterName(mcpRoute *aigv1a1.MCPRoute, backendName gwapiv1.ObjectName) string {
460-
return fmt.Sprintf("%s%s-%s", internalapi.MCPBackendFilterPrefix, mcpRoute.Name, backendName)
520+
return fmt.Sprintf("%s%s-%s", internalapi.MCPPerBackendHTTPRouteFilterPrefix, mcpRoute.Name, backendName)
461521
}
462522

463523
// mcpBackendRefToHTTPRouteRule creates an HTTPRouteRule for the given MCPRouteBackendRef.

internal/controller/mcp_route_security_policy.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (c *MCPRouteController) syncMCPRouteSecurityPolicy(ctx context.Context, mcp
7979
// ensureAccessTokenSecurityPolicy ensures that the SecurityPolicy resource exists with JWT authentication to validate access tokens.
8080
func (c *MCPRouteController) ensureAccessTokenSecurityPolicy(ctx context.Context, mcpRoute *aigv1a1.MCPRoute, httpRouteName string) error {
8181
var securityPolicy egv1a1.SecurityPolicy
82-
securityPolicyName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name
82+
securityPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name
8383
err := c.client.Get(ctx, client.ObjectKey{Name: securityPolicyName, Namespace: mcpRoute.Namespace}, &securityPolicy)
8484
existingPolicy := err == nil
8585

@@ -497,7 +497,7 @@ func (c *MCPRouteController) buildOAuthAuthServerMetadataJSON(oauth *aigv1a1.MCP
497497
// cleanupSecurityPolicyResources deletes existing SecurityPolicy-related resources when SecurityPolicy is nil.
498498
func (c *MCPRouteController) cleanupSecurityPolicyResources(ctx context.Context, mcpRoute *aigv1a1.MCPRoute) error {
499499
// Delete SecurityPolicy.
500-
securityPolicyName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name
500+
securityPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name
501501
var securityPolicy egv1a1.SecurityPolicy
502502
err := c.client.Get(ctx, client.ObjectKey{Name: securityPolicyName, Namespace: mcpRoute.Namespace}, &securityPolicy)
503503
if err == nil {
@@ -637,11 +637,11 @@ func fetchOAuthAuthServerMetadata(authServer string) (*OAuthAuthServerMetadata,
637637
}
638638

639639
func oauthProtectedResourceMetadataName(mcpRouteName string) string {
640-
return fmt.Sprintf("%s%s%s", internalapi.MCPHTTPRoutePrefix, mcpRouteName, oauthProtectedResourceMetadataSuffix)
640+
return fmt.Sprintf("%s%s%s", internalapi.MCPGeneratedResourceCommonPrefix, mcpRouteName, oauthProtectedResourceMetadataSuffix)
641641
}
642642

643643
func oauthAuthServerMetadataFilterName(mcpRouteName string) string {
644-
return fmt.Sprintf("%s%s%s", internalapi.MCPHTTPRoutePrefix, mcpRouteName, oauthAuthServerMetadataSuffix)
644+
return fmt.Sprintf("%s%s%s", internalapi.MCPGeneratedResourceCommonPrefix, mcpRouteName, oauthAuthServerMetadataSuffix)
645645
}
646646

647647
// discoverJWKSURI attempts to discover the JWKS URI from the OAuth authorization server metadata.

internal/controller/mcp_route_security_policy_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func TestMCPRouteController_syncMCPRouteSecurityPolicy(t *testing.T) {
203203
}
204204
require.NoError(t, err)
205205

206-
securityPolicyName := internalapi.MCPHTTPRoutePrefix + tt.mcpRoute.Name
206+
securityPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + tt.mcpRoute.Name
207207
var securityPolicy egv1a1.SecurityPolicy
208208
secPolErr := fakeClient.Get(t.Context(), client.ObjectKey{Name: securityPolicyName, Namespace: tt.mcpRoute.Namespace}, &securityPolicy)
209209

@@ -223,7 +223,7 @@ func TestMCPRouteController_syncMCPRouteSecurityPolicy(t *testing.T) {
223223
require.Error(t, secPolErr, "SecurityPolicy should not exist")
224224
}
225225

226-
backendTrafficPolicyName := internalapi.MCPHTTPRoutePrefix + tt.mcpRoute.Name + oauthProtectedResourceMetadataSuffix
226+
backendTrafficPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + tt.mcpRoute.Name + oauthProtectedResourceMetadataSuffix
227227
var backendTrafficPolicy egv1a1.BackendTrafficPolicy
228228
btpErr := fakeClient.Get(t.Context(), client.ObjectKey{Name: backendTrafficPolicyName, Namespace: tt.mcpRoute.Namespace}, &backendTrafficPolicy)
229229

@@ -233,7 +233,7 @@ func TestMCPRouteController_syncMCPRouteSecurityPolicy(t *testing.T) {
233233
require.Error(t, btpErr, "BackendTrafficPolicy should not exist")
234234
}
235235

236-
httpRouteFilterName := internalapi.MCPHTTPRoutePrefix + tt.mcpRoute.Name + oauthProtectedResourceMetadataSuffix
236+
httpRouteFilterName := internalapi.MCPGeneratedResourceCommonPrefix + tt.mcpRoute.Name + oauthProtectedResourceMetadataSuffix
237237
var httpRouteFilter egv1a1.HTTPRouteFilter
238238
filterErr := fakeClient.Get(t.Context(), client.ObjectKey{Name: httpRouteFilterName, Namespace: tt.mcpRoute.Namespace}, &httpRouteFilter)
239239

@@ -280,10 +280,10 @@ func TestMCPRouteControllerCleanupSecurityPolicyResources(t *testing.T) {
280280
err = c.syncMCPRouteSecurityPolicy(t.Context(), mcpRoute, httpRouteName)
281281
require.NoError(t, err)
282282

283-
securityPolicyName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name
284-
backendTrafficPolicyName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name + oauthProtectedResourceMetadataSuffix
285-
protecedResourceMetadataFilterName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name + oauthProtectedResourceMetadataSuffix
286-
authServerMetadataFilterName := internalapi.MCPHTTPRoutePrefix + mcpRoute.Name + oauthAuthServerMetadataSuffix
283+
securityPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name
284+
backendTrafficPolicyName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name + oauthProtectedResourceMetadataSuffix
285+
protecedResourceMetadataFilterName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name + oauthProtectedResourceMetadataSuffix
286+
authServerMetadataFilterName := internalapi.MCPGeneratedResourceCommonPrefix + mcpRoute.Name + oauthAuthServerMetadataSuffix
287287

288288
var securityPolicy egv1a1.SecurityPolicy
289289
err = fakeClient.Get(t.Context(), client.ObjectKey{Name: securityPolicyName, Namespace: mcpRoute.Namespace}, &securityPolicy)

0 commit comments

Comments
 (0)