Skip to content

Commit c9b9099

Browse files
committed
Add auth configs for path-based routing
Signed-off-by: Dan Barr <[email protected]>
1 parent c2fdd9d commit c9b9099

File tree

1 file changed

+245
-24
lines changed

1 file changed

+245
-24
lines changed

docs/toolhive/guides-k8s/connect-clients.mdx

Lines changed: 245 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ service.
150150

151151
:::note
152152

153-
Path rewriting syntax varies by Ingress controller. Check your controller's
154-
documentation for the correct annotations or resources.
153+
Path rewriting support and syntax varies by Ingress controller. Check your
154+
controller's documentation for the correct annotations or resources.
155155

156156
:::
157157

@@ -175,18 +175,20 @@ spec:
175175
- host: mcp.example.com
176176
http:
177177
paths:
178+
# Fetch MCP server
178179
- path: /fetch
179180
pathType: Prefix
180181
backend:
181182
service:
182183
name: mcp-fetch-proxy
183184
port:
184185
number: 8080
185-
- path: /weather
186+
# Another MCP server
187+
- path: /<SERVER_NAME>
186188
pathType: Prefix
187189
backend:
188190
service:
189-
name: mcp-weather-proxy
191+
name: mcp-<SERVER_NAME>-proxy
190192
port:
191193
number: 8080
192194
---
@@ -200,11 +202,117 @@ spec:
200202
stripPrefix:
201203
prefixes:
202204
- /fetch
203-
- /weather
205+
- /<SERVER_NAME>
204206
```
205207

206208
The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and
207-
`https://mcp.example.com/weather/mcp`.
209+
`https://mcp.example.com/<SERVER_NAME>/mcp`.
210+
211+
</TabItem>
212+
<TabItem value='path-based-auth' label='Path-based routing with OAuth'>
213+
214+
This example is the same as the previous path-based routing example but includes
215+
additional rules in the Ingress to direct the `.well-known` path for each MCP
216+
server to the corresponding backend. This is necessary when using OAuth
217+
authentication since the OAuth flow requires access to the `.well-known`
218+
endpoint for discovery.
219+
220+
First, in the MCPServer spec for each server, ensure the `resourceUrl` property
221+
is set to the full client-facing URL:
222+
223+
```yaml title="fetch-server-oauth.yaml"
224+
apiVersion: toolhive.stacklok.dev/v1alpha1
225+
kind: MCPServer
226+
# ...
227+
spec:
228+
oidcConfig:
229+
type: inline
230+
resourceUrl: https://mcp.example.com/<SERVER_NAME>/mcp
231+
inline:
232+
# ... other OIDC config ...
233+
```
234+
235+
The `inline.audience` value should match the audience expected by your identity
236+
provider, and is likely the same for all servers using the same authorization
237+
server. See [Authentication and authorization](./auth-k8s.mdx) for full OIDC
238+
setup instructions.
239+
240+
Configure the Ingress with additional rules for the `.well-known` paths of each
241+
MCP server that has OAuth enabled:
242+
243+
:::note
244+
245+
Path rewriting support and syntax varies by Ingress controller. Check your
246+
controller's documentation for the correct annotations or resources.
247+
248+
:::
249+
250+
```yaml {28-34,43-49} title="mcp-ingress.yaml"
251+
apiVersion: networking.k8s.io/v1
252+
kind: Ingress
253+
metadata:
254+
name: mcp-servers-ingress
255+
namespace: toolhive-system
256+
annotations:
257+
cert-manager.io/cluster-issuer: 'letsencrypt-prod'
258+
# Traefik example: strip path prefix
259+
traefik.ingress.kubernetes.io/router.middlewares: toolhive-system-strip-mcp-prefix@kubernetescrd
260+
spec:
261+
ingressClassName: traefik
262+
tls:
263+
- hosts:
264+
- mcp.example.com
265+
secretName: mcp-tls
266+
rules:
267+
- host: mcp.example.com
268+
http:
269+
paths:
270+
# Fetch MCP server
271+
- path: /fetch
272+
pathType: Prefix
273+
backend:
274+
service:
275+
name: mcp-fetch-proxy
276+
port:
277+
number: 8080
278+
- path: /.well-known/oauth-protected-resource/fetch/mcp
279+
pathType: Exact
280+
backend:
281+
service:
282+
name: mcp-fetch-proxy
283+
port:
284+
number: 8080
285+
# Another MCP server
286+
- path: /<SERVER_NAME>
287+
pathType: Prefix
288+
backend:
289+
service:
290+
name: mcp-<SERVER_NAME>-proxy
291+
port:
292+
number: 8080
293+
- path: /.well-known/oauth-protected-resource/<SERVER_NAME>/mcp
294+
pathType: Exact
295+
backend:
296+
service:
297+
name: mcp-<SERVER_NAME>-proxy
298+
port:
299+
number: 8080
300+
---
301+
# Traefik Middleware to strip path prefixes
302+
apiVersion: traefik.io/v1alpha1
303+
kind: Middleware
304+
metadata:
305+
name: strip-mcp-prefix
306+
namespace: toolhive-system
307+
spec:
308+
stripPrefix:
309+
prefixes:
310+
- /fetch
311+
- /<SERVER_NAME>
312+
```
313+
314+
The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and
315+
`https://mcp.example.com/<SERVER_NAME>/mcp`.
208316

209317
</TabItem>
210318
</Tabs>
@@ -300,7 +408,7 @@ metadata:
300408
namespace: toolhive-system
301409
spec:
302410
parentRefs:
303-
- name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing)
411+
- name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway)
304412
# namespace: default # Uncomment if Gateway is in a different namespace
305413
hostnames:
306414
- fetch-mcp.example.com # Change to your domain
@@ -327,11 +435,94 @@ metadata:
327435
namespace: toolhive-system
328436
spec:
329437
parentRefs:
330-
- name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing)
438+
- name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway)
439+
# namespace: default # Uncomment if Gateway is in a different namespace
440+
hostnames:
441+
- mcp.example.com # Change to your domain
442+
rules:
443+
# Fetch MCP server
444+
- matches:
445+
- path:
446+
type: PathPrefix
447+
value: /fetch
448+
filters:
449+
- type: URLRewrite
450+
urlRewrite:
451+
path:
452+
type: ReplacePrefixMatch
453+
replacePrefixMatch: /
454+
backendRefs:
455+
- name: mcp-fetch-proxy # Format: mcp-<mcpserver-name>-proxy
456+
port: 8080 # This matches the proxyPort
457+
# Another MCP server
458+
- matches:
459+
- path:
460+
type: PathPrefix
461+
value: /<SERVER_NAME>
462+
filters:
463+
- type: URLRewrite
464+
urlRewrite:
465+
path:
466+
type: ReplacePrefixMatch
467+
replacePrefixMatch: /
468+
backendRefs:
469+
- name: mcp-<SERVER_NAME>-proxy
470+
port: 8080
471+
```
472+
473+
The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and
474+
`https://mcp.example.com/<SERVER_NAME>/mcp`.
475+
476+
The `URLRewrite` filter removes the path prefix (e.g., `/fetch`) before
477+
forwarding requests to the backend service, so the MCP server receives requests
478+
at `/mcp` as expected.
479+
480+
</TabItem>
481+
<TabItem value='path-based-auth' label='Path-based routing with OAuth'>
482+
483+
This example is the same as the previous path-based routing example but includes
484+
additional rules in the HTTPRoute to direct the `.well-known` path for each MCP
485+
server to the corresponding backend. This is necessary when using OAuth
486+
authentication since the OAuth flow requires access to the `.well-known`
487+
endpoint for discovery.
488+
489+
First, in the MCPServer spec for each server, ensure the `resourceUrl` property
490+
is set to the full client-facing URL:
491+
492+
```yaml title="fetch-server-oauth.yaml"
493+
apiVersion: toolhive.stacklok.dev/v1alpha1
494+
kind: MCPServer
495+
# ...
496+
spec:
497+
oidcConfig:
498+
type: inline
499+
resourceUrl: https://mcp.example.com/<SERVER_NAME>/mcp
500+
inline:
501+
# ... other OIDC config ...
502+
```
503+
504+
The `inline.audience` value should match the audience expected by your identity
505+
provider, and is likely the same for all servers using the same authorization
506+
server. See [Authentication and authorization](./auth-k8s.mdx) for full OIDC
507+
setup instructions.
508+
509+
Configure the HTTPRoute with additional rules for the `.well-known` paths of
510+
each MCP server that has OAuth enabled:
511+
512+
```yaml {27-33,48-54} title="mcp-routes.yaml"
513+
apiVersion: gateway.networking.k8s.io/v1
514+
kind: HTTPRoute
515+
metadata:
516+
name: mcp-servers-route
517+
namespace: toolhive-system
518+
spec:
519+
parentRefs:
520+
- name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway)
331521
# namespace: default # Uncomment if Gateway is in a different namespace
332522
hostnames:
333523
- mcp.example.com # Change to your domain
334524
rules:
525+
# Fetch MCP server
335526
- matches:
336527
- path:
337528
type: PathPrefix
@@ -345,23 +536,38 @@ spec:
345536
backendRefs:
346537
- name: mcp-fetch-proxy # Format: mcp-<mcpserver-name>-proxy
347538
port: 8080 # This matches the proxyPort
539+
- matches:
540+
- path:
541+
type: Exact
542+
value: /.well-known/oauth-protected-resource/fetch/mcp
543+
backendRefs:
544+
- name: mcp-fetch-proxy
545+
port: 8080
546+
# Another MCP server
348547
- matches:
349548
- path:
350549
type: PathPrefix
351-
value: /weather
550+
value: /<SERVER_NAME>
352551
filters:
353552
- type: URLRewrite
354553
urlRewrite:
355554
path:
356555
type: ReplacePrefixMatch
357556
replacePrefixMatch: /
358557
backendRefs:
359-
- name: mcp-weather-proxy
558+
- name: mcp-<SERVER_NAME>-proxy
559+
port: 8080
560+
- matches:
561+
- path:
562+
type: Exact
563+
value: /.well-known/oauth-protected-resource/<SERVER_NAME>/mcp
564+
backendRefs:
565+
- name: mcp-<SERVER_NAME>-proxy
360566
port: 8080
361567
```
362568

363569
The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and
364-
`https://mcp.example.com/weather/mcp`.
570+
`https://mcp.example.com/<SERVER_NAME>/mcp`.
365571

366572
The `URLRewrite` filter removes the path prefix (e.g., `/fetch`) before
367573
forwarding requests to the backend service, so the MCP server receives requests
@@ -740,27 +946,42 @@ Common causes:
740946
</details>
741947

742948
<details>
743-
<summary>DNS resolution fails</summary>
949+
<summary>Authentication failures</summary>
744950

745-
If your domain does not resolve to your cluster:
951+
If OAuth authentication is failing when connecting to your MCP server:
746952

747953
```bash
748-
# Check Ingress external IP
749-
kubectl get ingress -n toolhive-system fetch-mcp-ingress
954+
# Test if the OAuth metadata endpoint is accessible
955+
# For host-based routing
956+
curl https://fetch-mcp.example.com/.well-known/oauth-protected-resource
750957
751-
# Test DNS resolution
752-
nslookup fetch-mcp.example.com
958+
# For path-based routing
959+
curl https://mcp.example.com/.well-known/oauth-protected-resource/fetch/mcp
753960
754-
# Or using dig
755-
dig fetch-mcp.example.com
961+
# Check proxy logs for authentication errors
962+
kubectl logs -n toolhive-system -l app.kubernetes.io/instance=fetch
963+
964+
# Verify the MCPServer OIDC configuration
965+
kubectl get mcpserver -n toolhive-system fetch -o yaml | grep -A15 oidcConfig
756966
```
757967

758-
Solutions:
968+
Common causes:
759969

760-
- Configure your DNS provider to create an A record pointing to the Ingress
761-
external IP
762-
- If using a cloud provider load balancer, create a CNAME record instead
763-
- Wait for DNS propagation (can take minutes to hours)
970+
- **`.well-known` endpoint not accessible**: When using path-based routing with
971+
OAuth, you must configure your Ingress or HTTPRoute to forward the
972+
`.well-known` path to the backend. See the "Path-based routing with OAuth" tab
973+
in the Ingress or Gateway API sections above for configuration examples.
974+
- **Missing or incorrect `resourceUrl`**: Ensure the `resourceUrl` in your
975+
MCPServer's `oidcConfig` matches the client-facing URL (e.g.,
976+
`https://mcp.example.com/fetch/mcp` for path-based routing).
977+
- **Token validation failure**: The token's `aud` claim must match the
978+
configured audience in your MCPServer's OIDC configuration.
979+
- **Issuer mismatch**: The token's `iss` claim must match the configured issuer.
980+
- **JWKS endpoint unreachable**: Check that the proxy can reach your identity
981+
provider's JWKS endpoint to validate tokens.
982+
983+
For detailed OAuth setup and troubleshooting, see the
984+
[Authentication and authorization](./auth-k8s.mdx) guide.
764985

765986
</details>
766987

0 commit comments

Comments
 (0)