From 9405c5b085b02117d5a47fbabd71c032b5e61af5 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:46:59 -0500 Subject: [PATCH 1/5] Add K8s client connectivity guide Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/toolhive/guides-k8s/connect-clients.mdx | 721 ++++++++++++++++++ docs/toolhive/guides-k8s/intro.mdx | 4 + docs/toolhive/guides-k8s/run-mcp-k8s.mdx | 7 +- docs/toolhive/tutorials/k8s-ingress-ngrok.mdx | 8 + sidebars.ts | 1 + 5 files changed, 739 insertions(+), 2 deletions(-) create mode 100644 docs/toolhive/guides-k8s/connect-clients.mdx diff --git a/docs/toolhive/guides-k8s/connect-clients.mdx b/docs/toolhive/guides-k8s/connect-clients.mdx new file mode 100644 index 00000000..a9ac17b6 --- /dev/null +++ b/docs/toolhive/guides-k8s/connect-clients.mdx @@ -0,0 +1,721 @@ +--- +title: Connect clients to MCP servers +description: + Learn how to connect clients to your Kubernetes-hosted MCP servers in + ToolHive. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Overview + +After deploying MCP servers in your Kubernetes cluster, you need to connect +clients to use them. This guide covers two main connection scenarios: + +1. **External clients** - Connecting from outside the cluster using Ingress or + Gateway API to expose MCP servers +2. **Internal clients** - Connecting from applications running within the same + Kubernetes cluster + +```mermaid +flowchart TB + subgraph External["External clients"] + UI["ToolHive UI"] + CLI["ToolHive CLI"] + Other["Other MCP clients"] + end + + subgraph K8s["Kubernetes cluster"] + Ingress["Ingress/Gateway"] + + subgraph NS["MCP Namespace"] + ProxyService["Service: MCP proxy"] + ProxyPod["Pod: ToolHive proxy"] + MCPPod["Pod: MCP server"] + end + + subgraph AppNS["App namespace"] + App["Your application"] + end + end + + UI -->|HTTPS| Ingress + CLI -->|HTTPS| Ingress + Other -->|HTTPS| Ingress + Ingress --> ProxyService + ProxyService --> ProxyPod + ProxyPod --> MCPPod + + App -->|HTTP| ProxyService +``` + +## Prerequisites + +- A Kubernetes cluster with MCP servers deployed (see + [Run MCP servers in Kubernetes](./run-mcp-k8s.mdx)) +- An Ingress controller or Gateway API implementation installed in your cluster + (for external access) +- [`kubectl`](https://kubernetes.io/docs/tasks/tools/) configured to communicate + with your cluster + +## Connect from outside the cluster + +To make your MCP servers accessible to external clients like the ToolHive UI, +ToolHive CLI, or other MCP clients, you need to expose the proxy service using +an Ingress resource or Gateway API. + +:::warning[Security requirements] + +When exposing MCP servers externally, you should: + +- Always use HTTPS with valid TLS certificates +- Configure authentication to control access (see + [Authentication and authorization](./auth-k8s.mdx)) +- Consider network policies to restrict traffic + +Running MCP servers without authentication on public networks is a security +risk. + +::: + +### Option 1: Using Ingress + +Ingress provides a stable API for exposing HTTP/HTTPS services. This example +shows a generic Ingress configuration that works with popular Ingress +controllers like Traefik, Contour, and HAProxy, as well as cloud provider +implementations like AWS Load Balancer Controller, Google Cloud Load Balancer, +and Azure Application Gateway. + +First, ensure you have an MCP server deployed. This example uses the `fetch` +server: + +```yaml title="fetch-server.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +metadata: + name: fetch + namespace: toolhive-system +spec: + image: ghcr.io/stackloklabs/gofetch/server:latest + transport: streamable-http + mcpPort: 8080 + proxyPort: 8080 +``` + +Create an Ingress resource to expose the MCP server proxy: + +```yaml title="fetch-ingress.yaml" +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: fetch-mcp-ingress + namespace: toolhive-system + annotations: + # Example: for cert-manager to provision certificates + cert-manager.io/cluster-issuer: 'letsencrypt-prod' + # Annotations vary by Ingress controller - check your controller's docs +spec: + ingressClassName: traefik # Change to match your Ingress controller + tls: + - hosts: + - fetch-mcp.example.com # Change to your domain + secretName: fetch-mcp-tls + rules: + - host: fetch-mcp.example.com # Change to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: mcp-fetch-proxy # Format: mcp--proxy + port: + number: 8080 # This matches the proxyPort +``` + +:::info[Service naming convention] + +The ToolHive operator automatically creates a Kubernetes Service for each +MCPServer following the naming pattern `mcp--proxy`. For example, an +MCPServer named `fetch` gets a Service named `mcp-fetch-proxy`. + +::: + +Apply both resources: + +```bash +kubectl apply -f fetch-server.yaml +kubectl apply -f fetch-ingress.yaml +``` + +Verify the Ingress is configured: + +```bash +kubectl get ingress -n toolhive-system +``` + +The Ingress should show your configured host and address. Once DNS is +configured, your MCP server is accessible at `https://fetch-mcp.example.com`. + +### Option 2: Using Gateway API + +The [Gateway API](https://gateway-api.sigs.k8s.io/) is a more expressive way to +expose services and is the successor to Ingress. This example works with Gateway +API implementations like Cilium, Istio, Envoy Gateway, and Traefik, as well as +cloud provider implementations like AWS Gateway API Controller, Google +Kubernetes Engine (GKE) Gateway controller, and Azure Application Gateway for +Containers. See the +[full list of implementations](https://gateway-api.sigs.k8s.io/implementations/). + +:::tip + +For a complete working example using the ngrok Gateway API implementation, see +the +[Configure secure ingress for MCP servers on Kubernetes](../tutorials/k8s-ingress-ngrok.mdx) +tutorial. + +::: + +**Check for an existing Gateway** + +Many Gateway API implementations create a Gateway resource automatically during +installation. For example, Traefik's Helm chart creates a `traefik-gateway` in +the default namespace when enabled. Check if a Gateway already exists: + +```bash +kubectl get gateway --all-namespaces +``` + +If a Gateway exists, note its name and namespace to use in your HTTPRoute. If +you need to create a new Gateway, use this example: + +```yaml title="mcp-gateway.yaml" +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: mcp-gateway + namespace: toolhive-system +spec: + gatewayClassName: traefik # Change to match your Gateway implementation + listeners: + - name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - name: mcp-gateway-cert + allowedRoutes: + namespaces: + from: Same +``` + +Create an HTTPRoute to expose your MCP server: + +```yaml title="fetch-route.yaml" +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: fetch-mcp-route + namespace: toolhive-system +spec: + parentRefs: + - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing) + # namespace: default # Uncomment if Gateway is in a different namespace + hostnames: + - fetch-mcp.example.com # Change to your domain + rules: + - backendRefs: + - name: mcp-fetch-proxy # Format: mcp--proxy + port: 8080 # This matches the proxyPort +``` + +Apply the resources: + +```bash +kubectl apply -f mcp-gateway.yaml +kubectl apply -f fetch-route.yaml +``` + +Verify the route is configured: + +```bash +kubectl get httproute -n toolhive-system +``` + +### TLS certificates + +For production deployments, use valid TLS certificates from a trusted +certificate authority. The most common approaches are: + + + + +[cert-manager](https://cert-manager.io/) automates certificate management in +Kubernetes. Install cert-manager and create a ClusterIssuer: + +```yaml title="letsencrypt-issuer.yaml" +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: your-email@example.com # Change to your email + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: traefik # Change to match your Ingress controller +``` + +Apply it: + +```bash +kubectl apply -f letsencrypt-issuer.yaml +``` + +The Ingress example above already includes the cert-manager annotation. Once +cert-manager is installed, it will automatically provision and renew +certificates. + + + + +If you have existing certificates, create a Kubernetes Secret: + +```bash +kubectl create secret tls fetch-mcp-tls \ + --cert=path/to/tls.crt \ + --key=path/to/tls.key \ + -n toolhive-system +``` + +Reference this secret in your Ingress or Gateway configuration as shown in the +examples above. + + + + +### Connect with ToolHive UI or CLI + +Once your MCP server is exposed with HTTPS, you can connect to it as a remote +MCP server from the ToolHive UI or CLI. + + + + +In the ToolHive UI: + +1. Click **Add an MCP server** on the **MCP Servers** page +2. Select **Add a remote MCP server** +3. Enter the connection details: + - **Name**: A friendly name for the server + - **Server URL**: `https://fetch-mcp.example.com/mcp` + - **Transport**: Streamable HTTP (or SSE if your server uses SSE) +4. If authentication is configured, select the method and enter the required + OAuth or OIDC details +5. Click **Install server** + +The MCP server appears in your server list and you can use it with any connected +MCP client. + + + + +Use the `thv run` command to connect: + +```bash +# Basic connection without authentication +thv run --name fetch-k8s https://fetch-mcp.example.com/mcp +``` + +If authentication is configured, add the appropriate flags. See the +[ToolHive CLI guide](../guides-cli/run-mcp-servers.mdx#authentication-setup) for +details. + +The MCP server is now available to your configured MCP clients. + + + + +For more details on using remote MCP servers, see: + +- [Run remote MCP servers (UI)](../guides-ui/run-mcp-servers.mdx?custom-type=custom_remote#install-a-custom-mcp-server) +- [Run remote MCP servers (CLI)](../guides-cli/run-mcp-servers.mdx#run-a-remote-mcp-server) + +## Connect from within the cluster + +Applications running inside your Kubernetes cluster can connect directly to MCP +server proxy services using Kubernetes service discovery. This is more efficient +and secure than routing through an external Ingress. + +### Service DNS names + +Each MCPServer automatically gets a Kubernetes Service that other pods can use +to connect. The service name follows the pattern `mcp--proxy`, and the +full DNS name follows the standard Kubernetes pattern: + +```text +mcp--proxy..svc.cluster.local: +``` + +For example, if you have an MCPServer named `fetch` in the `toolhive-system` +namespace with `proxyPort: 8080`, the full URL would be: + +```text +http://mcp-fetch-proxy.toolhive-system.svc.cluster.local:8080 +``` + +Within the same namespace, you can use the short form: + +```text +http://mcp-fetch-proxy:8080 +``` + +### Example: Configuring applications + +When deploying applications like AI agents in the same cluster, configure them +to use the service DNS name. This example shows how to pass the connection URL +as an environment variable: + +```yaml title="agent-app.yaml" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-agent-app + namespace: my-app +spec: + # ... other deployment configuration ... + template: + spec: + containers: + - name: app + image: my-agent-app:latest + env: + - name: MCP_SERVER_URL + # Different namespace: use full DNS name + value: 'http://mcp-fetch-proxy.toolhive-system.svc.cluster.local:8080/mcp' + # Same namespace: use short name + # value: 'http://mcp-fetch-proxy:8080/mcp' +``` + +### Network policies for cross-namespace access + +If your cluster uses network policies, you may need to create a policy to allow +traffic between namespaces: + +```yaml title="allow-cross-namespace.yaml" +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-app-to-mcp + namespace: toolhive-system +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: toolhive-proxy + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: my-app # Your app's namespace must have this label +``` + +## Authentication for external clients + +When exposing MCP servers externally, configure authentication to control +access. ToolHive supports multiple authentication methods: + +- **OIDC authentication** - Use external identity providers like Google, GitHub, + Okta, or Microsoft Entra ID +- **Kubernetes service accounts** - For service-to-service authentication within + the cluster + +See the [Authentication and authorization](./auth-k8s.mdx) guide for detailed +setup instructions. + +## Check connection status + +### Test external connectivity + +If you have the ToolHive CLI installed, you can test connectivity to your MCP +server: + +```bash +thv mcp list tools --server https://fetch-mcp.example.com/mcp +``` + +Or use `curl` to send a JSON-RPC request: + +```bash +curl -X POST https://fetch-mcp.example.com/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' +``` + +You should receive a JSON response with a list of available tools. + +:::tip + +If you've configured authentication on your MCP server, see the +[Authentication and authorization](./auth-k8s.mdx) guide for how to test +authenticated connections. + +::: + +### Test internal connectivity + +Test connectivity from within the cluster: + +```bash +# Port-forward to test locally +kubectl port-forward -n toolhive-system service/mcp-fetch-proxy 8080:8080 + +# In another terminal, test the connection +curl -X POST http://localhost:8080/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' +``` + +### Verify the connection path + +Check that the Ingress or Gateway is properly configured and the Service has +running pods: + +```bash +# For Ingress: verify it exists and has an address +kubectl get ingress -n toolhive-system fetch-mcp-ingress + +# For Gateway API: check HTTPRoute status (look for "Accepted: True" in conditions) +kubectl describe httproute -n toolhive-system fetch-mcp-route + +# Verify the Service exists +kubectl get service -n toolhive-system mcp-fetch-proxy + +# Check that the proxy pod is running +kubectl get pods -n toolhive-system -l app.kubernetes.io/instance=fetch +``` + +If the Ingress shows an address or the HTTPRoute status shows "Accepted: True", +and the pod is running, the connection path is properly configured. + +## Next steps + +Learn how to secure your MCP servers with +[Authentication and authorization](./auth-k8s.mdx). + +Configure [Telemetry and metrics](./telemetry-and-metrics.mdx) to monitor your +MCP server usage and performance. + +[Set up logging](./logging.mdx) to track requests and audit MCP server activity. + +## Related information + +- [Run MCP servers in Kubernetes](./run-mcp-k8s.mdx) - Deploy MCP servers in + your cluster +- [Proxy remote MCP servers](./remote-mcp-proxy.mdx) - Create proxies for + external MCP servers +- [Client compatibility](../reference/client-compatibility.mdx) - Supported MCP + clients and configuration +- [Kubernetes CRD reference](../reference/crd-spec.mdx) - Full MCPServer + specification +- [Configure secure ingress tutorial](../tutorials/k8s-ingress-ngrok.mdx) - + Complete tutorial using ngrok and Gateway API + +## Troubleshooting + +
+Ingress returns 503 Service Unavailable + +If your Ingress returns a 503 error: + +```bash +# Check if the service exists +kubectl get service -n toolhive-system mcp-fetch-proxy + +# Check the proxy pod is running +kubectl get pods -n toolhive-system -l app.kubernetes.io/instance=fetch + +# Check pod logs for errors +kubectl logs -n toolhive-system -l app.kubernetes.io/instance=fetch +``` + +Common causes: + +- **Proxy pod not running**: Ensure the MCPServer resource was created + successfully +- **Wrong service name**: The Ingress backend service name must follow the + pattern `mcp--proxy` +- **Wrong port**: The Ingress backend port must match the `proxyPort` in the + MCPServer spec +- **Pod health check failing**: Check the proxy pod logs for errors + +
+ +
+TLS certificate issues + +If you see certificate errors when connecting: + +```bash +# Check the certificate secret exists +kubectl get secret -n toolhive-system fetch-mcp-tls + +# Describe the secret to verify it contains tls.crt and tls.key +kubectl describe secret -n toolhive-system fetch-mcp-tls + +# If using cert-manager, check certificate status +kubectl get certificate -n toolhive-system + +# Check cert-manager logs +kubectl logs -n cert-manager -l app=cert-manager +``` + +Common causes: + +- **Certificate not ready**: Wait for cert-manager to provision the certificate + (can take a few minutes) +- **DNS not configured**: Ensure your domain points to the Ingress load balancer +- **Challenge validation failing**: Check cert-manager logs for ACME challenge + errors +- **Wrong ClusterIssuer**: Verify the cert-manager annotation references an + existing ClusterIssuer + +
+ +
+DNS resolution fails + +If your domain does not resolve to your cluster: + +```bash +# Check Ingress external IP +kubectl get ingress -n toolhive-system fetch-mcp-ingress + +# Test DNS resolution +nslookup fetch-mcp.example.com + +# Or using dig +dig fetch-mcp.example.com +``` + +Solutions: + +- Configure your DNS provider to create an A record pointing to the Ingress + external IP +- If using a cloud provider load balancer, create a CNAME record instead +- Wait for DNS propagation (can take minutes to hours) + +
+ +
+Cannot connect from within cluster + +If pods cannot connect to the MCP server service: + +```bash +# Verify service exists +kubectl get service -n toolhive-system mcp-fetch-proxy + +# Check that pods are running +kubectl get pods -n toolhive-system -l app.kubernetes.io/instance=fetch + +# Test DNS resolution from a pod +kubectl run test-dns -n toolhive-system --image=busybox --restart=Never -- \ + nslookup mcp-fetch-proxy.toolhive-system.svc.cluster.local + +# Check the DNS test results +kubectl logs -n toolhive-system test-dns + +# Clean up the test pod +kubectl delete pod -n toolhive-system test-dns + +# Test connectivity from a pod +kubectl run test-curl -n toolhive-system --image=curlimages/curl --restart=Never -- \ + curl -X POST http://mcp-fetch-proxy:8080/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' + +# Check the connectivity test results +kubectl logs -n toolhive-system test-curl + +# Clean up the test pod +kubectl delete pod -n toolhive-system test-curl +``` + +Common causes: + +- **Network policies blocking traffic**: Check for network policies that might + prevent pod-to-pod communication +- **Wrong namespace**: Ensure you're using the correct service DNS name for + cross-namespace access +- **Service not created**: The operator automatically creates services, but + verify it exists +- **Wrong port**: Ensure you're using the `proxyPort` value from the MCPServer + spec +- **Wrong service name**: Remember the service name is `mcp--proxy`, not + just `` + +
+ +
+Gateway API not working + +If using Gateway API and connections fail: + +```bash +# Check Gateway status +kubectl get gateway -n toolhive-system mcp-gateway + +# Check HTTPRoute status +kubectl get httproute -n toolhive-system fetch-mcp-route + +# Describe the HTTPRoute for detailed status +kubectl describe httproute -n toolhive-system fetch-mcp-route + +# Check Gateway implementation logs (example for Istio) +kubectl logs -n istio-system -l app=istio-ingressgateway +``` + +Common causes: + +- **Gateway not ready**: Wait for the Gateway to be accepted and programmed by + the controller +- **Wrong gateway class**: Ensure `gatewayClassName` matches your installed + Gateway API implementation +- **Listener configuration issues**: Verify the Gateway listener configuration + matches the HTTPRoute requirements +- **Certificate issues**: For HTTPS, ensure the certificate reference exists and + is valid + +
+ +
+Cross-namespace access denied + +If cross-namespace connections fail: + +```bash +# Check network policies in the MCP server namespace +kubectl get networkpolicy -n toolhive-system + +# Describe network policies to see rules +kubectl describe networkpolicy -n toolhive-system + +# Check if the app namespace has the required labels +kubectl get namespace my-app --show-labels +``` + +Solutions: + +- Create or update network policies to allow traffic from your app namespace +- Add required labels to your application namespace +- Test connectivity using a debug pod in the app namespace + +
diff --git a/docs/toolhive/guides-k8s/intro.mdx b/docs/toolhive/guides-k8s/intro.mdx index 78f854ac..b6b78f97 100644 --- a/docs/toolhive/guides-k8s/intro.mdx +++ b/docs/toolhive/guides-k8s/intro.mdx @@ -49,6 +49,10 @@ flowchart TB Client["MCP Client
[ex: Copilot]"] -- http --> Ingress ``` +The diagram shows how clients connect to MCP servers through an Ingress. To +learn how to expose your MCP servers and connect clients, see +[Connect clients to MCP servers](./connect-clients.mdx). + ## Installation [Use Helm to install the ToolHive operator](./deploy-operator-helm.mdx) in your diff --git a/docs/toolhive/guides-k8s/run-mcp-k8s.mdx b/docs/toolhive/guides-k8s/run-mcp-k8s.mdx index bf10c2df..6f3a2cf8 100644 --- a/docs/toolhive/guides-k8s/run-mcp-k8s.mdx +++ b/docs/toolhive/guides-k8s/run-mcp-k8s.mdx @@ -442,8 +442,11 @@ kubectl -n describe mcpserver ## Next steps -See the [Client compatibility](../reference/client-compatibility.mdx) reference -to learn how to connect to MCP servers using different clients. +Learn how to [connect clients to your MCP servers](./connect-clients.mdx) from +outside the cluster using Ingress or Gateway API, or from applications running +within the cluster. See the +[Client compatibility](../reference/client-compatibility.mdx) reference for +information about supported MCP clients. Learn how to customize MCP tools using [filters and overrides](./customize-tools.mdx). diff --git a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx index 24c46cfa..ca7a0994 100644 --- a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx +++ b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx @@ -20,6 +20,14 @@ Kubernetes gateway solutions that work with the Kubernetes Gateway API, such as and [many others](https://gateway-api.sigs.k8s.io/implementations/#implementations_1). +:::tip + +This tutorial demonstrates ngrok specifically. For general guidance on exposing +MCP servers with Ingress, Gateway API, or cloud provider implementations, see +[Connect clients to MCP servers](../guides-k8s/connect-clients.mdx). + +::: + ## What you'll learn This tutorial demonstrates how to make MCP servers available centrally for teams diff --git a/sidebars.ts b/sidebars.ts index fc0294e0..01f67025 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -153,6 +153,7 @@ const sidebars: SidebarsConfig = { 'toolhive/guides-k8s/intro', 'toolhive/guides-k8s/deploy-operator-helm', 'toolhive/guides-k8s/run-mcp-k8s', + 'toolhive/guides-k8s/connect-clients', 'toolhive/guides-k8s/remote-mcp-proxy', 'toolhive/guides-k8s/customize-tools', 'toolhive/guides-k8s/telemetry-and-metrics', From c2fdd9d07cec7bf3e7321e787776a76ab3d8d7c9 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:30:44 -0500 Subject: [PATCH 2/5] Add path-based routing Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/toolhive/guides-k8s/connect-clients.mdx | 188 +++++++++++++++++-- 1 file changed, 169 insertions(+), 19 deletions(-) diff --git a/docs/toolhive/guides-k8s/connect-clients.mdx b/docs/toolhive/guides-k8s/connect-clients.mdx index a9ac17b6..34538bf2 100644 --- a/docs/toolhive/guides-k8s/connect-clients.mdx +++ b/docs/toolhive/guides-k8s/connect-clients.mdx @@ -103,7 +103,14 @@ spec: proxyPort: 8080 ``` -Create an Ingress resource to expose the MCP server proxy: +Create an Ingress resource to expose the MCP server proxy. You can use either +host-based routing (separate subdomain per server) or path-based routing (single +domain with paths): + + + + +Each MCP server gets its own subdomain: ```yaml title="fetch-ingress.yaml" apiVersion: networking.k8s.io/v1 @@ -112,28 +119,96 @@ metadata: name: fetch-mcp-ingress namespace: toolhive-system annotations: - # Example: for cert-manager to provision certificates cert-manager.io/cluster-issuer: 'letsencrypt-prod' - # Annotations vary by Ingress controller - check your controller's docs spec: - ingressClassName: traefik # Change to match your Ingress controller + ingressClassName: traefik tls: - hosts: - - fetch-mcp.example.com # Change to your domain + - fetch-mcp.example.com secretName: fetch-mcp-tls rules: - - host: fetch-mcp.example.com # Change to your domain + - host: fetch-mcp.example.com http: paths: - path: / pathType: Prefix backend: service: - name: mcp-fetch-proxy # Format: mcp--proxy + name: mcp-fetch-proxy + port: + number: 8080 +``` + +The MCP server is accessible at `https://fetch-mcp.example.com/mcp`. + + + + +Multiple MCP servers share a single domain using path prefixes. This approach +requires URL rewriting to strip the path prefix before forwarding to the backend +service. + +:::note + +Path rewriting syntax varies by Ingress controller. Check your controller's +documentation for the correct annotations or resources. + +::: + +```yaml title="mcp-ingress.yaml" +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mcp-servers-ingress + namespace: toolhive-system + annotations: + cert-manager.io/cluster-issuer: 'letsencrypt-prod' + # Traefik example: strip path prefix + traefik.ingress.kubernetes.io/router.middlewares: toolhive-system-strip-mcp-prefix@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - mcp.example.com + secretName: mcp-tls + rules: + - host: mcp.example.com + http: + paths: + - path: /fetch + pathType: Prefix + backend: + service: + name: mcp-fetch-proxy + port: + number: 8080 + - path: /weather + pathType: Prefix + backend: + service: + name: mcp-weather-proxy port: - number: 8080 # This matches the proxyPort + number: 8080 +--- +# Traefik Middleware to strip path prefixes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-mcp-prefix + namespace: toolhive-system +spec: + stripPrefix: + prefixes: + - /fetch + - /weather ``` +The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and +`https://mcp.example.com/weather/mcp`. + + + + :::info[Service naming convention] The ToolHive operator automatically creates a Kubernetes Service for each @@ -142,11 +217,11 @@ MCPServer named `fetch` gets a Service named `mcp-fetch-proxy`. ::: -Apply both resources: +Apply the resources: ```bash kubectl apply -f fetch-server.yaml -kubectl apply -f fetch-ingress.yaml +kubectl apply -f fetch-ingress.yaml # or mcp-ingress.yaml for path-based ``` Verify the Ingress is configured: @@ -155,9 +230,6 @@ Verify the Ingress is configured: kubectl get ingress -n toolhive-system ``` -The Ingress should show your configured host and address. Once DNS is -configured, your MCP server is accessible at `https://fetch-mcp.example.com`. - ### Option 2: Using Gateway API The [Gateway API](https://gateway-api.sigs.k8s.io/) is a more expressive way to @@ -211,7 +283,14 @@ spec: from: Same ``` -Create an HTTPRoute to expose your MCP server: +Create an HTTPRoute to expose your MCP server. You can use either host-based +routing (separate subdomain per server) or path-based routing (single domain +with paths): + + + + +Each MCP server gets its own subdomain: ```yaml title="fetch-route.yaml" apiVersion: gateway.networking.k8s.io/v1 @@ -231,11 +310,71 @@ spec: port: 8080 # This matches the proxyPort ``` +The MCP server is accessible at `https://fetch-mcp.example.com/mcp`. + + + + +Multiple MCP servers share a single domain using path prefixes. This approach +uses URL rewriting to strip the path prefix before forwarding to the backend +service. + +```yaml title="mcp-routes.yaml" +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: mcp-servers-route + namespace: toolhive-system +spec: + parentRefs: + - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing) + # namespace: default # Uncomment if Gateway is in a different namespace + hostnames: + - mcp.example.com # Change to your domain + rules: + - matches: + - path: + type: PathPrefix + value: /fetch + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: mcp-fetch-proxy # Format: mcp--proxy + port: 8080 # This matches the proxyPort + - matches: + - path: + type: PathPrefix + value: /weather + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: mcp-weather-proxy + port: 8080 +``` + +The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and +`https://mcp.example.com/weather/mcp`. + +The `URLRewrite` filter removes the path prefix (e.g., `/fetch`) before +forwarding requests to the backend service, so the MCP server receives requests +at `/mcp` as expected. + + + + Apply the resources: ```bash -kubectl apply -f mcp-gateway.yaml -kubectl apply -f fetch-route.yaml +kubectl apply -f mcp-gateway.yaml # If creating a new Gateway +kubectl apply -f fetch-route.yaml # or mcp-routes.yaml for path-based ``` Verify the route is configured: @@ -249,7 +388,7 @@ kubectl get httproute -n toolhive-system For production deployments, use valid TLS certificates from a trusted certificate authority. The most common approaches are: - + [cert-manager](https://cert-manager.io/) automates certificate management in @@ -314,7 +453,9 @@ In the ToolHive UI: 2. Select **Add a remote MCP server** 3. Enter the connection details: - **Name**: A friendly name for the server - - **Server URL**: `https://fetch-mcp.example.com/mcp` + - **Server URL**: Use the appropriate URL based on your routing approach: + - Host-based: `https://fetch-mcp.example.com/mcp` + - Path-based: `https://mcp.example.com/fetch/mcp` - **Transport**: Streamable HTTP (or SSE if your server uses SSE) 4. If authentication is configured, select the method and enter the required OAuth or OIDC details @@ -329,8 +470,11 @@ MCP client. Use the `thv run` command to connect: ```bash -# Basic connection without authentication +# Host-based routing: separate subdomain per server thv run --name fetch-k8s https://fetch-mcp.example.com/mcp + +# Path-based routing: single domain with paths +thv run --name fetch-k8s https://mcp.example.com/fetch/mcp ``` If authentication is configured, add the appropriate flags. See the @@ -454,9 +598,15 @@ thv mcp list tools --server https://fetch-mcp.example.com/mcp Or use `curl` to send a JSON-RPC request: ```bash +# Host-based routing curl -X POST https://fetch-mcp.example.com/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' + +# Path-based routing +curl -X POST https://mcp.example.com/fetch/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` You should receive a JSON response with a list of available tools. From c9b909999837ba82170201685610ec1d6a17ba4d Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:40:29 -0500 Subject: [PATCH 3/5] Add auth configs for path-based routing Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/toolhive/guides-k8s/connect-clients.mdx | 269 +++++++++++++++++-- 1 file changed, 245 insertions(+), 24 deletions(-) diff --git a/docs/toolhive/guides-k8s/connect-clients.mdx b/docs/toolhive/guides-k8s/connect-clients.mdx index 34538bf2..34482fc0 100644 --- a/docs/toolhive/guides-k8s/connect-clients.mdx +++ b/docs/toolhive/guides-k8s/connect-clients.mdx @@ -150,8 +150,8 @@ service. :::note -Path rewriting syntax varies by Ingress controller. Check your controller's -documentation for the correct annotations or resources. +Path rewriting support and syntax varies by Ingress controller. Check your +controller's documentation for the correct annotations or resources. ::: @@ -175,6 +175,7 @@ spec: - host: mcp.example.com http: paths: + # Fetch MCP server - path: /fetch pathType: Prefix backend: @@ -182,11 +183,12 @@ spec: name: mcp-fetch-proxy port: number: 8080 - - path: /weather + # Another MCP server + - path: / pathType: Prefix backend: service: - name: mcp-weather-proxy + name: mcp--proxy port: number: 8080 --- @@ -200,11 +202,117 @@ spec: stripPrefix: prefixes: - /fetch - - /weather + - / ``` The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and -`https://mcp.example.com/weather/mcp`. +`https://mcp.example.com//mcp`. + + + + +This example is the same as the previous path-based routing example but includes +additional rules in the Ingress to direct the `.well-known` path for each MCP +server to the corresponding backend. This is necessary when using OAuth +authentication since the OAuth flow requires access to the `.well-known` +endpoint for discovery. + +First, in the MCPServer spec for each server, ensure the `resourceUrl` property +is set to the full client-facing URL: + +```yaml title="fetch-server-oauth.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +# ... +spec: + oidcConfig: + type: inline + resourceUrl: https://mcp.example.com//mcp + inline: + # ... other OIDC config ... +``` + +The `inline.audience` value should match the audience expected by your identity +provider, and is likely the same for all servers using the same authorization +server. See [Authentication and authorization](./auth-k8s.mdx) for full OIDC +setup instructions. + +Configure the Ingress with additional rules for the `.well-known` paths of each +MCP server that has OAuth enabled: + +:::note + +Path rewriting support and syntax varies by Ingress controller. Check your +controller's documentation for the correct annotations or resources. + +::: + +```yaml {28-34,43-49} title="mcp-ingress.yaml" +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mcp-servers-ingress + namespace: toolhive-system + annotations: + cert-manager.io/cluster-issuer: 'letsencrypt-prod' + # Traefik example: strip path prefix + traefik.ingress.kubernetes.io/router.middlewares: toolhive-system-strip-mcp-prefix@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - mcp.example.com + secretName: mcp-tls + rules: + - host: mcp.example.com + http: + paths: + # Fetch MCP server + - path: /fetch + pathType: Prefix + backend: + service: + name: mcp-fetch-proxy + port: + number: 8080 + - path: /.well-known/oauth-protected-resource/fetch/mcp + pathType: Exact + backend: + service: + name: mcp-fetch-proxy + port: + number: 8080 + # Another MCP server + - path: / + pathType: Prefix + backend: + service: + name: mcp--proxy + port: + number: 8080 + - path: /.well-known/oauth-protected-resource//mcp + pathType: Exact + backend: + service: + name: mcp--proxy + port: + number: 8080 +--- +# Traefik Middleware to strip path prefixes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-mcp-prefix + namespace: toolhive-system +spec: + stripPrefix: + prefixes: + - /fetch + - / +``` + +The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and +`https://mcp.example.com//mcp`. @@ -300,7 +408,7 @@ metadata: namespace: toolhive-system spec: parentRefs: - - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing) + - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway) # namespace: default # Uncomment if Gateway is in a different namespace hostnames: - fetch-mcp.example.com # Change to your domain @@ -327,11 +435,94 @@ metadata: namespace: toolhive-system spec: parentRefs: - - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway if using existing) + - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway) + # namespace: default # Uncomment if Gateway is in a different namespace + hostnames: + - mcp.example.com # Change to your domain + rules: + # Fetch MCP server + - matches: + - path: + type: PathPrefix + value: /fetch + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: mcp-fetch-proxy # Format: mcp--proxy + port: 8080 # This matches the proxyPort + # Another MCP server + - matches: + - path: + type: PathPrefix + value: / + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: mcp--proxy + port: 8080 +``` + +The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and +`https://mcp.example.com//mcp`. + +The `URLRewrite` filter removes the path prefix (e.g., `/fetch`) before +forwarding requests to the backend service, so the MCP server receives requests +at `/mcp` as expected. + + + + +This example is the same as the previous path-based routing example but includes +additional rules in the HTTPRoute to direct the `.well-known` path for each MCP +server to the corresponding backend. This is necessary when using OAuth +authentication since the OAuth flow requires access to the `.well-known` +endpoint for discovery. + +First, in the MCPServer spec for each server, ensure the `resourceUrl` property +is set to the full client-facing URL: + +```yaml title="fetch-server-oauth.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +# ... +spec: + oidcConfig: + type: inline + resourceUrl: https://mcp.example.com//mcp + inline: + # ... other OIDC config ... +``` + +The `inline.audience` value should match the audience expected by your identity +provider, and is likely the same for all servers using the same authorization +server. See [Authentication and authorization](./auth-k8s.mdx) for full OIDC +setup instructions. + +Configure the HTTPRoute with additional rules for the `.well-known` paths of +each MCP server that has OAuth enabled: + +```yaml {27-33,48-54} title="mcp-routes.yaml" +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: mcp-servers-route + namespace: toolhive-system +spec: + parentRefs: + - name: mcp-gateway # Reference your Gateway name (e.g., traefik-gateway) # namespace: default # Uncomment if Gateway is in a different namespace hostnames: - mcp.example.com # Change to your domain rules: + # Fetch MCP server - matches: - path: type: PathPrefix @@ -345,10 +536,18 @@ spec: backendRefs: - name: mcp-fetch-proxy # Format: mcp--proxy port: 8080 # This matches the proxyPort + - matches: + - path: + type: Exact + value: /.well-known/oauth-protected-resource/fetch/mcp + backendRefs: + - name: mcp-fetch-proxy + port: 8080 + # Another MCP server - matches: - path: type: PathPrefix - value: /weather + value: / filters: - type: URLRewrite urlRewrite: @@ -356,12 +555,19 @@ spec: type: ReplacePrefixMatch replacePrefixMatch: / backendRefs: - - name: mcp-weather-proxy + - name: mcp--proxy + port: 8080 + - matches: + - path: + type: Exact + value: /.well-known/oauth-protected-resource//mcp + backendRefs: + - name: mcp--proxy port: 8080 ``` The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and -`https://mcp.example.com/weather/mcp`. +`https://mcp.example.com//mcp`. The `URLRewrite` filter removes the path prefix (e.g., `/fetch`) before forwarding requests to the backend service, so the MCP server receives requests @@ -740,27 +946,42 @@ Common causes:
-DNS resolution fails +Authentication failures -If your domain does not resolve to your cluster: +If OAuth authentication is failing when connecting to your MCP server: ```bash -# Check Ingress external IP -kubectl get ingress -n toolhive-system fetch-mcp-ingress +# Test if the OAuth metadata endpoint is accessible +# For host-based routing +curl https://fetch-mcp.example.com/.well-known/oauth-protected-resource -# Test DNS resolution -nslookup fetch-mcp.example.com +# For path-based routing +curl https://mcp.example.com/.well-known/oauth-protected-resource/fetch/mcp -# Or using dig -dig fetch-mcp.example.com +# Check proxy logs for authentication errors +kubectl logs -n toolhive-system -l app.kubernetes.io/instance=fetch + +# Verify the MCPServer OIDC configuration +kubectl get mcpserver -n toolhive-system fetch -o yaml | grep -A15 oidcConfig ``` -Solutions: +Common causes: -- Configure your DNS provider to create an A record pointing to the Ingress - external IP -- If using a cloud provider load balancer, create a CNAME record instead -- Wait for DNS propagation (can take minutes to hours) +- **`.well-known` endpoint not accessible**: When using path-based routing with + OAuth, you must configure your Ingress or HTTPRoute to forward the + `.well-known` path to the backend. See the "Path-based routing with OAuth" tab + in the Ingress or Gateway API sections above for configuration examples. +- **Missing or incorrect `resourceUrl`**: Ensure the `resourceUrl` in your + MCPServer's `oidcConfig` matches the client-facing URL (e.g., + `https://mcp.example.com/fetch/mcp` for path-based routing). +- **Token validation failure**: The token's `aud` claim must match the + configured audience in your MCPServer's OIDC configuration. +- **Issuer mismatch**: The token's `iss` claim must match the configured issuer. +- **JWKS endpoint unreachable**: Check that the proxy can reach your identity + provider's JWKS endpoint to validate tokens. + +For detailed OAuth setup and troubleshooting, see the +[Authentication and authorization](./auth-k8s.mdx) guide.
From 523a2797651f242096b245f4b73e9384e4386aa1 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:19:29 -0500 Subject: [PATCH 4/5] Streamline ngrok tutorial and add auth addendum Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/toolhive/tutorials/k8s-ingress-ngrok.mdx | 162 ++++++------------ 1 file changed, 57 insertions(+), 105 deletions(-) diff --git a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx index ca7a0994..6baf0119 100644 --- a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx +++ b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx @@ -134,7 +134,7 @@ mcp-mkp-proxy ClusterIP 10.96.106.88 8080/TCP 2m19s Note the service name and port number for the next step. -## Step 3: Create an ngrok Gateway resource +## Step 3: Create Gateway and HTTPRoute resources Now, create a Gateway and HTTPRoute resource to expose the MCP server securely via ngrok. @@ -146,7 +146,7 @@ account, it will be in the format `.ngrok-free.app`. Replace Create a file named `ngrok-mcp-gateway.yaml` with the following content: -```yaml {12,29,35-37} title="ngrok-mcp-gateway.yaml" +```yaml {12} title="ngrok-mcp-gateway.yaml" apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -162,7 +162,11 @@ spec: allowedRoutes: namespaces: from: All ---- +``` + +Then create a file named `ngrok-mcp-httproute.yaml` with the following content: + +```yaml {13,20-22} title="ngrok-mcp-httproute.yaml" apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -186,10 +190,11 @@ spec: port: 8080 # Replace with your port number from Step 2 ``` -Apply the configuration to your cluster: +Apply the configurations to your cluster: ```bash kubectl apply -f ngrok-mcp-gateway.yaml +kubectl apply -f ngrok-mcp-httproute.yaml ``` ## Step 4: Access the MCP server @@ -204,6 +209,7 @@ Use the ToolHive CLI to verify connectivity to the MCP server: thv mcp list tools --server https:///mcp ``` +The `/mcp` path is the default endpoint for the MCP Streamable HTTP transport. The output should display a list of tools managed by the MCP server, confirming that you have successfully set up secure ingress using ngrok. For the "MKP" MCP server, you should see output similar to this: @@ -226,9 +232,11 @@ The MKP MCP server is now available to AI clients configured with ## Optional: Tunnel multiple MCP servers with URL rewrites -If you have multiple MCP servers and want to expose them via the same ngrok -Gateway, you can use URL rewrites in the `HTTPRoute` resource. This allows you -to route requests to different MCP servers based on path prefixes. +The previous steps exposed a single MCP server at the root path (`/`). If you +have multiple MCP servers and want to expose them via the same ngrok Gateway, +you can use path-based routing with URL rewrites in the HTTPRoute resource. This +allows you to route requests to different MCP servers based on path prefixes +like `/mkp` and `/fetch`. :::note @@ -243,39 +251,15 @@ Run a second MCP server, for example: kubectl apply -f https://raw.githubusercontent.com/stacklok/toolhive/refs/heads/main/examples/operator/mcp-servers/mcpserver_fetch.yaml ``` -Then, update the `ngrok-mcp-gateway.yaml` file. Update the `rules` section of -the existing `HTTPRoute` resource to give the MKP MCP server a path prefix of +Then, update the `ngrok-mcp-httproute.yaml` file. Update the `rules` section of +the existing HTTPRoute resource to give the MKP MCP server a path prefix of `/mkp` and add a `URLRewrite` filter. -```yaml {8,12-17} title="ngrok-mcp-gateway.yaml" -# ... existing HTTPRoute resource ... -spec: - # ... existing spec ... - rules: - - matches: - - path: - type: PathPrefix - value: /mkp - backendRefs: - - name: mcp-mkp-proxy - port: 8080 - filters: - - type: URLRewrite - urlRewrite: - path: - type: ReplacePrefixMatch - replacePrefixMatch: '' -``` - -Then, add a new rule for the Fetch MCP server with a path prefix of `/fetch`, -again replacing `` with your actual domain: - -```yaml {14,19,21-22} title="ngrok-mcp-gateway.yaml" ---- +```yaml {18,22-27} showLineNumbers title="ngrok-mcp-httproute.yaml" apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: fetch-mcp-route + name: mcp-servers-route namespace: toolhive-system spec: parentRefs: @@ -289,82 +273,22 @@ spec: - matches: - path: type: PathPrefix - value: /fetch + value: /mkp backendRefs: - - name: mcp-fetch-proxy + - name: mcp-mkp-proxy port: 8080 filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch - replacePrefixMatch: '' + replacePrefixMatch: '' # Normally this would be '/' but ngrok requires empty string ``` -At this point, your `ngrok-mcp-gateway.yaml` file should contain one `Gateway` -resource and two `HTTPRoute` resources. +Add another rule for the Fetch MCP server with a path prefix of `/fetch`: -
-Full example of updated ngrok-mcp-gateway.yaml -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: ngrok-gateway - namespace: toolhive-system -spec: - gatewayClassName: ngrok - listeners: - - name: https - protocol: HTTPS - port: 443 - hostname: - allowedRoutes: - namespaces: - from: All ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: mkp-mcp-route - namespace: toolhive-system -spec: - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: ngrok-gateway - namespace: toolhive-system - hostnames: - - - rules: - - matches: - - path: - type: PathPrefix - value: /mkp - backendRefs: - - name: mcp-mkp-proxy - port: 8080 - filters: - - type: URLRewrite - urlRewrite: - path: - type: ReplacePrefixMatch - replacePrefixMatch: "" ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: fetch-mcp-route - namespace: toolhive-system -spec: - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: ngrok-gateway - namespace: toolhive-system - hostnames: - - - rules: +{/* prettier-ignore */} +```yaml showLineNumbers=28 title="ngrok-mcp-httproute.yaml" - matches: - path: type: PathPrefix @@ -377,14 +301,13 @@ spec: urlRewrite: path: type: ReplacePrefixMatch - replacePrefixMatch: "" + replacePrefixMatch: '' ``` -
Apply the updated configuration to your cluster: ```bash -kubectl apply -f ngrok-mcp-gateway.yaml +kubectl apply -f ngrok-mcp-httproute.yaml ``` You can now access both MCP servers using the same ngrok domain with different @@ -408,7 +331,8 @@ To remove the ngrok resources from your cluster and ngrok account, run the following: ```bash -# Delete the Gateway and HTTPRoute resources +# Delete the HTTPRoute and Gateway resources +kubectl delete -f ngrok-mcp-httproute.yaml kubectl delete -f ngrok-mcp-gateway.yaml # Delete the ngrok CRDs @@ -430,3 +354,31 @@ next steps: server usage and performance. - Try other gateway solutions like Traefik or Istio if they're already part of your infrastructure. + +## Addendum: Combining with MCP server authentication + +When exposing MCP servers via ngrok or any other ingress solution, consider the +security implications. While ngrok provides secure HTTPS tunnels, you should +also implement authentication and authorization to control access. The ToolHive +Operator supports +[OAuth-based authentication methods](../guides-k8s/auth-k8s.mdx) that are out of +scope for this tutorial but essential for production deployments. + +When OAuth is enabled on an MCP server, add additional rules to the HTTPRoute +resource to expose the OAuth metadata endpoint for proper authentication flow +through the gateway. + +Here's an example rule to add to the `mcp-servers-route` HTTPRoute in your +`ngrok-mcp-httproute.yaml` file. Add this rule alongside the existing `/mkp` +path rule: + +{/* prettier-ignore */} +```yaml title="ngrok-mcp-httproute.yaml" + - matches: + - path: + type: Exact + value: /.well-known/oauth-protected-resource/mkp + backendRefs: + - name: mcp-mkp-proxy + port: 8080 +``` From 302fa76a7d6d0e95ca9dc3efc08c69d9513851d3 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:32:57 -0500 Subject: [PATCH 5/5] Apply review suggestions Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/toolhive/guides-k8s/connect-clients.mdx | 18 ++++++++---------- docs/toolhive/tutorials/k8s-ingress-ngrok.mdx | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/toolhive/guides-k8s/connect-clients.mdx b/docs/toolhive/guides-k8s/connect-clients.mdx index 34482fc0..c66c5d8d 100644 --- a/docs/toolhive/guides-k8s/connect-clients.mdx +++ b/docs/toolhive/guides-k8s/connect-clients.mdx @@ -65,6 +65,14 @@ To make your MCP servers accessible to external clients like the ToolHive UI, ToolHive CLI, or other MCP clients, you need to expose the proxy service using an Ingress resource or Gateway API. +:::info[Service naming convention] + +The ToolHive operator automatically creates a Kubernetes Service for each +MCPServer following the naming pattern `mcp--proxy`. For example, an +MCPServer named `fetch` gets a Service named `mcp-fetch-proxy`. + +::: + :::warning[Security requirements] When exposing MCP servers externally, you should: @@ -317,14 +325,6 @@ The MCP servers are accessible at `https://mcp.example.com/fetch/mcp` and
-:::info[Service naming convention] - -The ToolHive operator automatically creates a Kubernetes Service for each -MCPServer following the naming pattern `mcp--proxy`. For example, an -MCPServer named `fetch` gets a Service named `mcp-fetch-proxy`. - -::: - Apply the resources: ```bash @@ -357,8 +357,6 @@ tutorial. ::: -**Check for an existing Gateway** - Many Gateway API implementations create a Gateway resource automatically during installation. For example, Traefik's Helm chart creates a `traefik-gateway` in the default namespace when enabled. Check if a Gateway already exists: diff --git a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx index 6baf0119..5540d9b4 100644 --- a/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx +++ b/docs/toolhive/tutorials/k8s-ingress-ngrok.mdx @@ -377,7 +377,7 @@ path rule: - matches: - path: type: Exact - value: /.well-known/oauth-protected-resource/mkp + value: /.well-known/oauth-protected-resource/mkp/mcp backendRefs: - name: mcp-mkp-proxy port: 8080