DEV Community

Yolanda Robla Mota for Stacklok

Posted on

Deploying an Okta-Authenticated BigQuery MCP Server on Kubernetes with ToolHive

In my previous article, I showed how to connect Okta authentication to a BigQuery MCP server running locally. The objective was to build a workflow that was secure (with user-level attribution and least privilege roles), short-lived, and that would save you the pain of managing Google service-account keys. That setup worked perfectly for local development, but it wasn’t something I’d confidently hand off to production.
This time, we’ll take that local prototype and transform it into a production-ready, cloud-native deployment running on Kubernetes, secured by Okta, and managed end-to-end by the ToolHive Operator. We’ll even make it accessible remotely through ngrok, so you can connect to it from anywhere using VS Code.

Setting the Stage

Before diving in, let’s make sure we have the right pieces in place. You’ll need a Kubernetes cluster (I’ll be using kind for simplicity), along with kubectl and helm. You’ll also need an Okta account with an authorization server configured, and a Google Cloud project with BigQuery enabled.
If you haven’t already, set up Workload Identity Federation in your Google Cloud project. That’s what allows Google Cloud to trust Okta tokens and issue temporary credentials for BigQuery access.
Finally, install the ToolHive CLI (thv) and sign up for an ngrok account — we’ll use both to expose your service later on.

Deploying the ToolHive Operator

Let’s start by getting the ToolHive Operator running in our cluster. The operator is what manages the lifecycle of MCP servers — it handles the pods, proxies, authentication, and updates automatically.
I’m using kind to create a local cluster:

kind create cluster --name toolhive
Enter fullscreen mode Exit fullscreen mode

Next, install the ToolHive CRDs and the operator itself:

helm upgrade --install toolhive-operator-crds \
  oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds

helm upgrade --install toolhive-operator \
  oci://ghcr.io/stacklok/toolhive/toolhive-operator \
  --namespace toolhive-system --create-namespace
Enter fullscreen mode Exit fullscreen mode

A quick check confirms the operator is running:

kubectl get pods -n toolhive-system
Enter fullscreen mode Exit fullscreen mode

You should see something like:

toolhive-operator-7875c8c5cd-xxxxx   1/1     Running   0   30s
Enter fullscreen mode Exit fullscreen mode

With that, our cluster is ready to start managing MCP servers.

Storing the Okta Secret

The next step is to give ToolHive access to your Okta client secret. This allows the proxy to validate incoming tokens. Instead of hardcoding secrets, Kubernetes encourages us to store them in a dedicated Secret resource.
Here’s the YAML to create one:

apiVersion: v1
kind: Secret
metadata:
  name: okta-client-secret
  namespace: default
type: Opaque
stringData:
  client-secret: <YOUR_OKTA_CLIENT_SECRET>
Enter fullscreen mode Exit fullscreen mode

Save that as 00-okta-client-secret.yaml and apply it:

kubectl apply -f 00-okta-client-secret.yaml
Enter fullscreen mode Exit fullscreen mode

Setting Up Token Exchange

To allow Okta to exchange its tokens for Google Cloud credentials, we’ll define an MCPExternalAuthConfig resource. This tells ToolHive how to talk to Google’s Security Token Service (STS) and request access tokens for BigQuery.
Here’s the config:

apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
  name: bigquery-token-exchange
  namespace: default
spec:
  type: tokenExchange
  tokenExchange:
    tokenUrl: https://sts.googleapis.com/v1/token
    audience: //iam.googleapis.com/projects/<YOUR_PROJECT_NUMBER>/locations/global/workloadIdentityPools/okta-pool/providers/okta-provider
    subjectTokenType: id_token
    scopes:
      - https://www.googleapis.com/auth/bigquery
      - https://www.googleapis.com/auth/cloud-platform
Enter fullscreen mode Exit fullscreen mode

Apply it with:

kubectl apply -f 01-external-auth-config.yaml
Enter fullscreen mode Exit fullscreen mode

This configuration acts as a bridge between Okta and Google Cloud, handling the secure exchange behind the scenes.

Deploying the BigQuery MCP Server

Now we can create the MCP server that will connect VS Code to BigQuery. This configuration ties together the image, authentication, and proxy.
We need to expose a public endpoint that is the resourceURL. For that, we can use a service like ngrok. Configure a domain in the ngrok dashboard or note your automatically-generated “dev domain” if you’re on a free account. Configure that properly on the custom resource, along with the other settings indicated with :

apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
  name: database-toolbox-bigquery
  namespace: default
spec:
  image: us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.19.1
  env:
    - name: BIGQUERY_PROJECT
      value: <YOUR_GCP_PROJECT_ID>
    - name: BIGQUERY_USE_CLIENT_OAUTH
      value: "true"

  args:
    - --prebuilt
    - bigquery
    - --address
    - 0.0.0.0

  transport: streamable-http
  proxyPort: 8000
  mcpPort: 5000

  oidcConfig:
    type: inline
    resourceUrl: https://<YOUR_NGROK_DOMAIN>.ngrok-free.app/mcp   # Replace with your ngrok URL
    inline:
      issuer: https://<YOUR_OKTA_DOMAIN>.okta.com/oauth2/<YOUR_AUTH_SERVER_ID>
      audience: //iam.googleapis.com/projects/<YOUR_PROJECT_NUMBER>/locations/global/workloadIdentityPools/okta-pool/providers/okta-provider
      clientId: <YOUR_OKTA_CLIENT_ID>
      clientSecretRef:
        name: okta-client-secret
        key: client-secret

  externalAuthConfigRef:
    name: bigquery-token-exchange

  resources:
    limits:
      cpu: "1"
      memory: "512Mi"
    requests:
      cpu: "100m"
      memory: "128Mi"
Enter fullscreen mode Exit fullscreen mode

Apply it with:

kubectl apply -f 02-mcp-server-bigquery.yaml
Enter fullscreen mode Exit fullscreen mode

Kubernetes will create two pods: one running the MCP server, and another running the ToolHive proxy.

Exposing the Service Publicly

Once the MCP server is running, we can expose it publicly to be reachable by authentication endpoints and clients. This means we’ll temporarily expose the service, create a tunnel through ngrok using ToolHive’s built-in support, and grab that domain before proceeding.
Start by forwarding the proxy service locally:

kubectl port-forward -n default svc/database-toolbox-bigquery-proxy-svc 8000:8000
Enter fullscreen mode Exit fullscreen mode

This makes the MCP proxy accessible at http://127.0.0.1:8000.

Now, use the ToolHive CLI to open a secure tunnel with ngrok:

thv proxy tunnel http://127.0.0.1:8000 tunnel \
  --tunnel-provider ngrok \
  --provider-args '{"auth-token": "<YOUR_NGROK_AUTH_TOKEN>", “url”: “https://<YOUR_NGROK_DOMAIN>.ngrok-free.app”}'
Enter fullscreen mode Exit fullscreen mode

ToolHive will create the tunnel and print a line like:

✔ Tunnel created
Public URL: https://<YOUR_NGROK_DOMAIN>.ngrok-free.app
Enter fullscreen mode Exit fullscreen mode

If you want more background on this tunneling feature, the ToolHive team has a nice write-up: Exposing a Kubernetes-Hosted MCP Server with ToolHive + ngrok (with Basic Auth)

Verifying the Deployment

After a few moments, confirm everything’s running:

kubectl get pods -n default -l toolhive-name=database-toolbox-bigquery
Enter fullscreen mode Exit fullscreen mode

You should see two pods in the “Running” state — one for the server, one for the proxy.
If you’d like to peek under the hood, tail the proxy logs to see the authentication and token exchange process in action:

kubectl logs -n default -l app.kubernetes.io/instance=database-toolbox-bigquery-proxy --tail=50
Enter fullscreen mode Exit fullscreen mode

You should see debug lines referencing token validation and the STS endpoint.

Connect from VS Code

Once your MCP server is running, secured, and exposed via your public ngrok URL (for example: https://abc123.ngrok-free.app/mcp), you’ll use VS Code’s MCP support to connect.

  • Open VS Code. Make sure you have the MCP / Copilot Chat extension installed and enabled.

  • Open the Command Palette (Ctrl+Shift+P or ⌘+Shift+P) and run “MCP: Add Server” (or you can open the mcp.json configuration manually).

  • When prompted, enter a JSON configuration like this:

{
  "servers": {
    "toolbox": {
      "url": "https://<YOUR_NGROK_DOMAIN>.ngrok-free.app/mcp",
      "type": "http"
    }
  },
  "inputs": []
}
Enter fullscreen mode Exit fullscreen mode

The "type": "http" indicates you’re connecting over HTTP transport.

  • After saving/accepting this config, VS Code will attempt to connect to the MCP server. During this process it will prompt you to enter the Client ID and the Client Secret from your Okta app

  • These credentials allow VS Code to authenticate and authorize with the server according to the MCP/OIDC handshake.

  • Once the authentication completes, the server will appear in your MCP server list. You can open the Chat view, select the MCP tools (e.g., query_bigquery, list_datasets, etc.), and issue queries or commands as needed.

  • Try a test query to confirm everything is working:

BigQuery with VSCode

Wrapping Up

We’ve come a long way from a local Okta-authenticated server to a fully managed, cloud-ready Kubernetes deployment. Now you have a secure, scalable, and remote-accessible BigQuery MCP server managed entirely by ToolHive.
This setup combines Okta’s identity management, Google Cloud’s token exchange, and Kubernetes automation into a single cohesive workflow. The result is a developer-friendly environment that’s easy to scale and safe to expose beyond your local machine.
If you’re interested in exploring further, join the ToolHive Discord community to share what you’ve built. The possibilities with ToolHive, Okta, and Kubernetes together are just getting started.

Top comments (0)