<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Yolanda Robla Mota</title>
    <description>The latest articles on Forem by Yolanda Robla Mota (@yrobla).</description>
    <link>https://forem.com/yrobla</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1402306%2Ff615da0c-ae47-4afb-95d7-95bf1873093f.jpeg</url>
      <title>Forem: Yolanda Robla Mota</title>
      <link>https://forem.com/yrobla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/yrobla"/>
    <language>en</language>
    <item>
      <title>Deploying an Okta-Authenticated BigQuery MCP Server on Kubernetes with ToolHive</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Wed, 19 Nov 2025 09:56:11 +0000</pubDate>
      <link>https://forem.com/stacklok/deploying-an-okta-authenticated-bigquery-mcp-server-on-kubernetes-with-toolhive-cf5</link>
      <guid>https://forem.com/stacklok/deploying-an-okta-authenticated-bigquery-mcp-server-on-kubernetes-with-toolhive-cf5</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/stacklok/how-to-use-okta-to-remotely-authenticate-to-your-bigquery-mcp-server-5a35"&gt;previous article&lt;/a&gt;, 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.&lt;br&gt;
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 &lt;strong&gt;ToolHive Operator&lt;/strong&gt;. We’ll even make it accessible remotely through &lt;strong&gt;ngrok&lt;/strong&gt;, so you can connect to it from anywhere using VS Code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting the Stage
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;br&gt;
I’m using &lt;em&gt;kind&lt;/em&gt; to create a local cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kind create cluster --name toolhive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the ToolHive CRDs and the operator itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick check confirms the operator is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pods -n toolhive-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;toolhive-operator-7875c8c5cd-xxxxx   1/1     Running   0   30s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, our cluster is ready to start managing MCP servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing the Okta Secret
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
Here’s the YAML to create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: okta-client-secret
  namespace: default
type: Opaque
stringData:
  client-secret: &amp;lt;YOUR_OKTA_CLIENT_SECRET&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that as &lt;em&gt;00-okta-client-secret.yaml&lt;/em&gt; and apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f 00-okta-client-secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up Token Exchange
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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/&amp;lt;YOUR_PROJECT_NUMBER&amp;gt;/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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f 01-external-auth-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration acts as a bridge between Okta and Google Cloud, handling the secure exchange behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying the BigQuery MCP Server
&lt;/h2&gt;

&lt;p&gt;Now we can create the MCP server that will connect VS Code to BigQuery. This configuration ties together the image, authentication, and proxy.&lt;br&gt;
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 &lt;a href="https://dashboard.ngrok.com/domains" rel="noopener noreferrer"&gt;ngrok dashboard&lt;/a&gt; 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 &lt;em&gt;&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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: &amp;lt;YOUR_GCP_PROJECT_ID&amp;gt;
    - 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://&amp;lt;YOUR_NGROK_DOMAIN&amp;gt;.ngrok-free.app/mcp   # Replace with your ngrok URL
    inline:
      issuer: https://&amp;lt;YOUR_OKTA_DOMAIN&amp;gt;.okta.com/oauth2/&amp;lt;YOUR_AUTH_SERVER_ID&amp;gt;
      audience: //iam.googleapis.com/projects/&amp;lt;YOUR_PROJECT_NUMBER&amp;gt;/locations/global/workloadIdentityPools/okta-pool/providers/okta-provider
      clientId: &amp;lt;YOUR_OKTA_CLIENT_ID&amp;gt;
      clientSecretRef:
        name: okta-client-secret
        key: client-secret

  externalAuthConfigRef:
    name: bigquery-token-exchange

  resources:
    limits:
      cpu: "1"
      memory: "512Mi"
    requests:
      cpu: "100m"
      memory: "128Mi"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f 02-mcp-server-bigquery.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes will create two pods: one running the MCP server, and another running the ToolHive proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing the Service Publicly
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
Start by forwarding the proxy service locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward -n default svc/database-toolbox-bigquery-proxy-svc 8000:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the MCP proxy accessible at &lt;a href="http://127.0.0.1:8000" rel="noopener noreferrer"&gt;http://127.0.0.1:8000&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, use the ToolHive CLI to open a secure tunnel with ngrok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv proxy tunnel http://127.0.0.1:8000 tunnel \
  --tunnel-provider ngrok \
  --provider-args '{"auth-token": "&amp;lt;YOUR_NGROK_AUTH_TOKEN&amp;gt;", “url”: “https://&amp;lt;YOUR_NGROK_DOMAIN&amp;gt;.ngrok-free.app”}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ToolHive will create the tunnel and print a line like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Tunnel created
Public URL: https://&amp;lt;YOUR_NGROK_DOMAIN&amp;gt;.ngrok-free.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want more background on this tunneling feature, the ToolHive team has a nice write-up: &lt;a href="https://dev.to/stacklok/exposing-a-kubernetes-hosted-mcp-server-with-toolhive-ngrok-with-basic-auth-23kn"&gt;Exposing a Kubernetes-Hosted MCP Server with ToolHive + ngrok (with Basic Auth)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Deployment
&lt;/h2&gt;

&lt;p&gt;After a few moments, confirm everything’s running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pods -n default -l toolhive-name=database-toolbox-bigquery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl logs -n default -l app.kubernetes.io/instance=database-toolbox-bigquery-proxy --tail=50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see debug lines referencing token validation and the STS endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect from VS Code
&lt;/h2&gt;

&lt;p&gt;Once your MCP server is running, secured, and exposed via your public ngrok URL (for example: &lt;em&gt;&lt;a href="https://abc123.ngrok-free.app/mcp" rel="noopener noreferrer"&gt;https://abc123.ngrok-free.app/mcp&lt;/a&gt;&lt;/em&gt;), you’ll use VS Code’s MCP support to connect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open VS Code. Make sure you have the MCP / Copilot Chat extension installed and enabled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the Command Palette (&lt;em&gt;Ctrl+Shift+P or ⌘+Shift+P&lt;/em&gt;) and run “&lt;strong&gt;MCP: Add Server&lt;/strong&gt;” (or you can open the &lt;em&gt;mcp.json&lt;/em&gt; configuration manually).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When prompted, enter a JSON configuration like this:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "servers": {
    "toolbox": {
      "url": "https://&amp;lt;YOUR_NGROK_DOMAIN&amp;gt;.ngrok-free.app/mcp",
      "type": "http"
    }
  },
  "inputs": []
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "type": "http" indicates you’re connecting over HTTP transport.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;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 &lt;strong&gt;Client ID&lt;/strong&gt; and the &lt;strong&gt;Client Secret&lt;/strong&gt; from your Okta app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;These credentials allow VS Code to authenticate and authorize with the server according to the MCP/OIDC handshake.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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., &lt;em&gt;query_bigquery, list_datasets&lt;/em&gt;, etc.), and issue queries or commands as needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try a test query to confirm everything is working:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppvjr181vjwpzub099tn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppvjr181vjwpzub099tn.png" alt="BigQuery with VSCode" width="512" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We’ve come a long way from a local Okta-authenticated server to a fully managed, cloud-ready Kubernetes deployment. Now you have a &lt;strong&gt;secure, scalable, and remote-accessible&lt;/strong&gt; BigQuery MCP server managed entirely by ToolHive.&lt;br&gt;
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.&lt;br&gt;
If you’re interested in exploring further, join the &lt;a href="https://discord.gg/stacklok" rel="noopener noreferrer"&gt;ToolHive Discord community&lt;/a&gt; to share what you’ve built. The possibilities with ToolHive, Okta, and Kubernetes together are just getting started.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>security</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to use Okta to remotely authenticate to your BigQuery MCP Server</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Thu, 06 Nov 2025 12:07:50 +0000</pubDate>
      <link>https://forem.com/stacklok/how-to-use-okta-to-remotely-authenticate-to-your-bigquery-mcp-server-5a35</link>
      <guid>https://forem.com/stacklok/how-to-use-okta-to-remotely-authenticate-to-your-bigquery-mcp-server-5a35</guid>
      <description>&lt;p&gt;This article builds on our &lt;a href="https://dev.to/stacklok/beyond-api-keys-token-exchange-identity-federation-mcp-servers-5dm8"&gt;previous post&lt;/a&gt;, where we explored the high-level architecture of token exchange, identity federation, and how to run MCP servers in a secure and IdP-agnostic way. Now we shift into the &lt;strong&gt;hands-on phase&lt;/strong&gt;: how to use ToolHive to enable an MCP server to query Google BigQuery for users authenticated via Okta. While we use Okta and Google Cloud as the example stack, this flow is adaptable to any IdP and any cloud provider with a compatible STS / federation service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You run an MCP server that receives requests from users who are authenticated via Okta.&lt;/li&gt;
&lt;li&gt;The MCP server must execute queries in Google Cloud BigQuery.&lt;/li&gt;
&lt;li&gt;You don’t want to manage Google service-account keys, embed JSON credentials in config, or lose per-user audit.&lt;/li&gt;
&lt;li&gt;You want: user-level attribution, least-privilege roles, secure, short-lived access, and federation between Okta and Google Cloud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, we’re implementing the IdP federation approach described as scenario “B” in the previous blog post. The diagram below shows how ToolHive, Okta, and Google Cloud interact in this flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0xcpaqqelbznbpgu9gw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0xcpaqqelbznbpgu9gw.png" alt="IDP federation diagram" width="512" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Okta admin access&lt;/strong&gt;: You’ll need permissions to create an OIDC app and an authorization server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Google Cloud project&lt;/strong&gt;: With BigQuery enabled and permissions to create a Workforce Identity Pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ToolHive CLI&lt;/strong&gt;: &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;download it from toolhive.dev&lt;/a&gt; and confirm it’s in your system path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container runtime&lt;/strong&gt;: Docker, Podman, or Rancher Desktop are supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP client&lt;/strong&gt; such as Claude Code (or any other client supporting the MCP protocol).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detailed configuration steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Configure Okta as Identity Provider
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the Okta Admin Console, navigate to &lt;strong&gt;Applications → Applications&lt;/strong&gt; and click &lt;strong&gt;Create App Integration&lt;/strong&gt;. See &lt;a href="https://help.okta.com/en-us/content/topics/apps/apps_app_integration_wizard_oidc.htm" rel="noopener noreferrer"&gt;https://help.okta.com/en-us/content/topics/apps/apps_app_integration_wizard_oidc.htm
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;OIDC – OpenID Connect&lt;/strong&gt; and then &lt;strong&gt;Web Application&lt;/strong&gt; for the app type.&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;sign-in redirect URI&lt;/strong&gt; to &lt;a href="http://localhost:8666/callback" rel="noopener noreferrer"&gt;http://localhost:8666/callback&lt;/a&gt; (this is the callback needed for the MCP server that we will run later using ToolHive).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;IMPORTANT: Note the client ID and client secret; you’ll need them in later steps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtr0jgbe98fiexu72szt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtr0jgbe98fiexu72szt.png" alt="Okta client" width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create an Authorization Server in Okta
&lt;/h3&gt;

&lt;p&gt;Your OIDC app issues tokens via an Authorization Server. For the Workforce Federation and token exchange, you need one configured correctly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Okta Admin Console, Navigate to &lt;strong&gt;Security → API → Authorization Servers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click Add Authorization Server.&lt;/li&gt;
&lt;li&gt;Name: &lt;strong&gt;BigQuery MCP Server&lt;/strong&gt; (or any descriptive name)&lt;/li&gt;
&lt;li&gt;Audience: set this to match the audience expected by your MCP server configuration (for example, &lt;strong&gt;mcpserver&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Click Save.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure an additional &lt;strong&gt;gcp.access&lt;/strong&gt; scope:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnc2ioab9kfw90o0sr8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnc2ioab9kfw90o0sr8o.png" alt="Okta scopes" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And the access policies for the types of tokens to generate, including Token Exchange:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7f25dmq25vyxdku6815l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7f25dmq25vyxdku6815l.png" alt="Okta rules" width="800" height="1452"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this setup, Okta will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issue standards-compliant OIDC tokens to your MCP server through ToolHive.&lt;/li&gt;
&lt;li&gt;Include the claims Google Cloud expects during the token exchange.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IMPORTANT: Note the issuer URL for the Authorization Server, you’ll need it in the next steps.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create Workforce Identity Pool in Google Cloud
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the Google Cloud console, create a &lt;strong&gt;Workforce Identity Pool&lt;/strong&gt; and a matching provider, using the Issuer URL you noted in the previous step:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzqku8asoiz3nmawsfyc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzqku8asoiz3nmawsfyc.png" alt="Workforce identity pool" width="800" height="717"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define custom audiences. The Okta client ID needs to be passed as an audience, so start by copying the default audience. Then select &lt;strong&gt;Allowed audiences&lt;/strong&gt;, add the default value, and include your Okta client ID as well.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F366pso8eb4y1lckuthdu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F366pso8eb4y1lckuthdu.png" alt="Allowed audiences" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure permissions for the Okta user so they can read BigQuery data. Repeat this for each user you want to map:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcloud projects add-iam-policy-binding &amp;lt;PROJECT_NAME&amp;gt; \
--member="principalSet://iam.googleapis.com/projects/&amp;lt;PROJECT_ID&amp;gt;/locations/global/workloadIdentityPools/okta-pool/attribute.email/&amp;lt;MAPPED_OKTA_EMAIL&amp;gt;" \
--role="roles/bigquery.dataViewer"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Deploy MCP server + proxy with remote authentication via ToolHive
&lt;/h3&gt;

&lt;p&gt;In this step, we bring together the MCP server and the remote authentication/federation flow. Using ToolHive, we’ll run the server and wrap it with a proxy that handles user authentication with Okta and token exchange into Google Cloud.&lt;/p&gt;

&lt;p&gt;Start by creating a group. ToolHive automatically manages clients registered to your default group, adding or removing MCP servers as you run them. Since this server will sit behind an authenticated proxy, we don’t want that auto-configuration behavior, so we’ll create a separate group for it instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv group create toolbox-group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start the open source &lt;a href="https://github.com/googleapis/genai-toolbox" rel="noopener noreferrer"&gt;MCP Toolbox for Databases&lt;/a&gt; server using the ToolHive CLI. ToolHive automatically pulls the server image using metadata from the ToolHive registry. You can view details about the image with &lt;code&gt;thv registry info database-toolbox&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv run --group toolbox-group database-toolbox \
--env BIGQUERY_PROJECT=&amp;lt;YOUR_PROJECT_ID&amp;gt; \
--env BIGQUERY_USE_CLIENT_OAUTH=true \
--proxy-port 6000 \
-- --prebuilt bigquery --address 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what each parameter does:&lt;br&gt;
&lt;strong&gt;--group toolbox-group&lt;/strong&gt;: Name of the ToolHive group that the MCP server belongs to&lt;br&gt;
&lt;strong&gt;database-toolbox&lt;/strong&gt;: The MCP server image from the ToolHive registry&lt;br&gt;
&lt;strong&gt;--env BIQUERY_PROJECT&lt;/strong&gt;: Your Google Cloud project ID containing BigQuery resources&lt;br&gt;
&lt;strong&gt;--env BIGQUERY_USE_CLIENT_OAUTH=true&lt;/strong&gt;: Use the OAuth flow instead of static service account credentials&lt;br&gt;
&lt;strong&gt;--proxy-port&lt;/strong&gt;: Port exposed on your host for the containerized MCP server&lt;br&gt;
&lt;strong&gt;--&lt;/strong&gt;: CLI arguments passed into the MCP server&lt;br&gt;
&lt;strong&gt;--prebuilt bigquery&lt;/strong&gt;: Use the prebuilt configuration for BigQuery&lt;br&gt;
&lt;strong&gt;--address 0.0.0.0&lt;/strong&gt;: Bind the server to all network interfaces so the proxy can reach it&lt;/p&gt;

&lt;p&gt;ToolHive spins up the MCP server container and HTTP proxy process, ready to handle BigQuery queries using the MCP protocol. Using &lt;a href="http://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; ensures the server is containerized, isolated, and managed securely — avoiding the “run-it-manually” friction.&lt;/p&gt;

&lt;p&gt;Next, the &lt;code&gt;thv proxy&lt;/code&gt; command starts a proxy process that sits in front of the MCP server and handles all incoming requests. It prompts you to sign in with Okta, exchanges your Okta token for a Google Cloud access token, and then forwards your request to the MCP server using that token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv proxy \
  --target-uri http://127.0.0.1:6000 \
  --remote-auth-client-id &amp;lt;OKTA_CLIENT_ID&amp;gt; \
  --remote-auth-client-secret &amp;lt;OKTA_CLIENT_SECRET&amp;gt; \
  --remote-auth okta \
  --remote-auth-issuer &amp;lt;AUTHORIZATION_SERVER_URL&amp;gt; \
  --remote-auth-callback-port 8666 \
  --remote-auth-scopes 'openid,profile,email,gcp.access' \
  --port 62614 \
  --token-exchange-url https://sts.googleapis.com/v1/token \
  --token-exchange-scopes 'https://www.googleapis.com/auth/bigquery,https://www.googleapis.com/auth/cloud-platform' \
  --token-exchange-audience //iam.googleapis.com/projects/&amp;lt;GOOGLE_PROJECT_NUMBER&amp;gt;/locations/global/workloadIdentityPools/okta-pool/providers/okta-provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what each flag does:&lt;br&gt;
&lt;strong&gt;--target-uri&lt;/strong&gt;: Points to the MCP server’s proxy port (from the previous step)&lt;br&gt;
&lt;strong&gt;--remote-auth-client-id&lt;/strong&gt;: Client ID of your Okta app (from step 1)&lt;br&gt;
&lt;strong&gt;--remote-auth-client-secret&lt;/strong&gt;: Client secret of your Okta app (from step 1)&lt;br&gt;
&lt;strong&gt;--remote-auth okta&lt;/strong&gt;: Specifies the remote auth provider&lt;br&gt;
&lt;strong&gt;--remote-auth-issuer&lt;/strong&gt;: URL of the Okta authorization server’s issuer (from step 2)&lt;br&gt;
&lt;strong&gt;--remote-auth-callback-port&lt;/strong&gt;: Local port used for the OAuth callback (must match the callback URL used in step 1)&lt;br&gt;
&lt;strong&gt;--remote-auth-scopes&lt;/strong&gt;: Scopes requested from Okta during authentication&lt;br&gt;
&lt;strong&gt;--port&lt;/strong&gt;: Port the ToolHive proxy exposes to clients&lt;br&gt;
&lt;strong&gt;--token-exchange-url&lt;/strong&gt;: Google STS endpoint for exchanging tokens&lt;br&gt;
&lt;strong&gt;--token-exchange-scopes&lt;/strong&gt;: Google Cloud scopes required to access BigQuery and related APIs&lt;br&gt;
&lt;strong&gt;--token-exchange-audience&lt;/strong&gt;: Google Workload Identity Pool audience for Okta federation&lt;/p&gt;

&lt;p&gt;When your browser opens, sign in with Okta. The proxy uses your Okta credentials to generate ID tokens, exchange them for valid Google tokens with the right scopes, and then continues the request automatically.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Run the MCP server with Claude or another client
&lt;/h3&gt;

&lt;p&gt;Let’s use Claude Code as an example. Because ToolHive doesn’t automatically manage client configurations for proxied MCP servers, you’ll need to add it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Add the authenticated ToolHive proxy
claude mcp add --scope user --transport http database-toolbox http://127.0.0.1:62614/mcp

# Run Claude Code
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Toolbox MCP server uses the token provided by the ToolHive proxy and passes it to Google Cloud, giving you access to the resources available to your account.&lt;/p&gt;

&lt;p&gt;Any other MCP-compatible client can connect the same way. Just point it to the ToolHive proxy endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2lcbdunl8b64c8skcip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2lcbdunl8b64c8skcip.png" alt="Claude and MCP" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this architecture is powerful
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple for clients&lt;/strong&gt;: Apps connect to the ToolHive proxy just like any other MCP server endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure authentication flow&lt;/strong&gt;: The proxy makes you log in through Okta, so every request carries a verified user identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federated access to Google Cloud&lt;/strong&gt;: Instead of embedding service account keys in your server, the proxy handles a token exchange so Google recognizes your identity through the workforce identity provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least-privilege and auditable&lt;/strong&gt;: BigQuery jobs run under your federated Okta identity, so logs show “&lt;a href="mailto:user@domain.com"&gt;user@domain.com&lt;/a&gt; ran a BigQuery job” rather than “service-account X”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns&lt;/strong&gt;: The MCP server (Toolbox) focuses on data tools and queries, while the proxy handles auth, token exchange, and routing. It’s a cleaner, safer architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, it’s easy to get started with ToolHive, since it’s free and open source. I encourage you to visit &lt;a href="https://toolhive.dev/" rel="noopener noreferrer"&gt;toolhive.dev&lt;/a&gt;, where you can download the project and explore our docs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Using Token Exchange with ToolHive and Okta for MCP Server to GraphQL Authentication</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Tue, 04 Nov 2025 16:37:21 +0000</pubDate>
      <link>https://forem.com/stacklok/using-token-exchange-with-toolhive-and-okta-for-mcp-server-to-graphql-authentication-3ehi</link>
      <guid>https://forem.com/stacklok/using-token-exchange-with-toolhive-and-okta-for-mcp-server-to-graphql-authentication-3ehi</guid>
      <description>&lt;p&gt;This article builds on our &lt;a href="https://dev.to/stacklok/beyond-api-keys-token-exchange-identity-federation-mcp-servers-5dm8"&gt;previous post&lt;/a&gt;, where we introduced the core concepts of token exchange and its role in secure authentication. Here, we delve into a practical application, demonstrating how to leverage Okta and ToolHive to facilitate token exchange for authenticating an MCP server with a GraphQL API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;p&gt;This demo mimics a (hopefully!) real world example where we run an API service and we want to expose it with an MCP server. The back end API requires a token with &lt;em&gt;aud=backend&lt;/em&gt; and &lt;em&gt;scopes=[backend-api:read]&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;"Aud" (audience) in a token specifies the intended recipient of the token, indicating which service or application is meant to consume it. "Scopes" define the specific permissions or access rights granted by the token, detailing what actions the token holder is authorized to perform. Only tokens having the expected audience and the expected scopes authorize the caller to use the service.&lt;/p&gt;

&lt;p&gt;We don’t want to expose the back end service directly to the AI client, but only through the MCP server. We also want to maintain a clean audit trail showing us who accessed what.&lt;/p&gt;

&lt;p&gt;The MCP server requires a token with &lt;em&gt;aud=mcpserver&lt;/em&gt; and &lt;em&gt;scopes=mcp:tools:call&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Both the API service and the MCP server are part of the same Okta realm, but we’ll use different Authorization Servers to ensure that both the token the MCP server receives and the token use different audiences.&lt;/p&gt;

&lt;p&gt;We’ll simulate the whole flow as a developer connecting to this setup by adding the MCP server to VSCode and calling the tools it provides.&lt;/p&gt;

&lt;p&gt;It should be noted that in this example, we’ll be using an &lt;a href="https://www.apollographql.com/docs" rel="noopener noreferrer"&gt;Apollo&lt;/a&gt;-based GraphQL service as the backend API service and the existing &lt;a href="https://www.apollographql.com/docs/apollo-mcp-server" rel="noopener noreferrer"&gt;Apollo MCP server&lt;/a&gt;, but the same setup applies to any kind of API services as long as they both use OAuth tokens from the same realm as the authentication mechanism. &lt;/p&gt;

&lt;p&gt;In order to follow along, you can clone the Apollo GraphQL service from &lt;a href="https://github.com/StacklokLabs/apollo-mcp-auth-demo" rel="noopener noreferrer"&gt;a demo repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okta setup
&lt;/h2&gt;

&lt;p&gt;I’ve used the Okta integrator setup to prepare this demo and therefore the instructions cover the whole setup from the ground up including creating the Authorization Servers. This is likely not needed or needs to be adjusted in a real world environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization Servers
&lt;/h3&gt;

&lt;p&gt;To logically separate the MCP server from the back end API service, we’ll configure two Okta Authorization servers - one for the MCP server and client and the other for the backend server. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyrkj0nwo8nmto911fag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyrkj0nwo8nmto911fag.png" alt="Okta authorization servers" width="512" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the Authorization Servers and then the following scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mcpserver AS &lt;em&gt;mcp:tools:call&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;backend AS &lt;em&gt;backend-api:read&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Trust between authorization servers
&lt;/h4&gt;

&lt;p&gt;In order to enable token exchange between two authorization servers - the one that issues tokens for access to the MCP server and the one that issues tokens for accessing the back end, we need to establish trust between the two.&lt;/p&gt;

&lt;p&gt;Go to the back end AS and down at the settings tab, add the mcpserver AS as trusted:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wycf3a7pyhzfjua0719.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wycf3a7pyhzfjua0719.png" alt="Okta trusted server" width="419" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Applications
&lt;/h3&gt;

&lt;p&gt;We’ll set up two Applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;em&gt;VSCode client&lt;/em&gt; to authenticate to the MCP server. We create a client directly to avoid Dynamic Client registration. This will be an OIDC application with a client ID and a secret. It is important to match the Redirect URIs that VSCode uses. Set the Redirect URIs to &lt;a href="http://127.0.0.1:33418" rel="noopener noreferrer"&gt;http://127.0.0.1:33418&lt;/a&gt; and &lt;a href="https://vscode.dev/redirect" rel="noopener noreferrer"&gt;https://vscode.dev/redirect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;toolhive client&lt;/em&gt; that will perform the Token Exchange. This is an API Services type in Okta lingo. To create the application, go to:&lt;/li&gt;
&lt;li&gt;Applications -&amp;gt; Create App Integration and select API Services&lt;/li&gt;
&lt;li&gt;Name your application&lt;/li&gt;
&lt;li&gt;In the application page, navigate to the General Settings page and uncheck the “Require Demonstrating Proof of Possession” header as this is not yet supported by ToolHive&lt;/li&gt;
&lt;li&gt;Check the Token Exchange grant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfkajllk1la4o93y0vve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfkajllk1la4o93y0vve.png" alt="Token exchange grant" width="478" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Policies
&lt;/h3&gt;

&lt;p&gt;In order for applications to authenticate, we need to include them in policies, otherwise Okta will not issue tokens to the clients. We’ll define two policies: One that allows the MCP Client (VSCode) to request tokens with &lt;em&gt;mcp:tools:call&lt;/em&gt; and another one that allows the token exchange by the ToolHive process.&lt;/p&gt;

&lt;h4&gt;
  
  
  MCP client to MCP server
&lt;/h4&gt;

&lt;p&gt;This policy is to be defined on the mcpserver AS side. Select “Add New Access Policy”, then “Assign to the following Clients” and select the VSCode client. When the policy is created, click “Add Rule” in the policy and in the “And the following scopes” section add both the “OpenID Connect” scopes and the &lt;em&gt;mcp:tools:call&lt;/em&gt; scopes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22mxe559jdbd901o637c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22mxe559jdbd901o637c.png" alt="Scopes" width="512" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  MCP server token exchange
&lt;/h4&gt;

&lt;p&gt;This policy is to be defined on the back end AS side. Select “Add New Access Policy”, then “Assign to the following Clients” and select the ToolHive client. When adding the rule, don’t forget to unroll “Advanced” under the “If Grant Type Is” section and add Token Exchange. Add “&lt;em&gt;backend-api:read&lt;/em&gt;” to the scopes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed28ypvi5nioio3fpu5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed28ypvi5nioio3fpu5z.png" alt="Scopes" width="512" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8uarrmw0krmln9zc3vi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8uarrmw0krmln9zc3vi.png" alt="Token exchange" width="512" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the GraphQL server
&lt;/h3&gt;

&lt;p&gt;Let’s clone our server locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/StacklokLabs/apollo-mcp-auth-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s configure the IDP settings in the .env file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp .env.example .env
vim .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using my Okta integrator account, the .env file looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Okta Configuration
# Your Okta domain (e.g., dev-123456.okta.com)
OKTA_DOMAIN=integrator-3683736.okta.com

# Your Okta issuer URL (authorization server)
# For default authorization server: https://your-domain.okta.com/oauth2/default
# For custom authorization server: https://your-domain.okta.com/oauth2/{authServerId}
OKTA_ISSUER=https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697

# JWT Validation Configuration
# Expected audience in JWT tokens (space-separated if multiple)
OKTA_AUDIENCE=backend
# Required scopes in JWT tokens (space-separated)
REQUIRED_SCOPES=backend-api:read

# Authentication Configuration
# Set to 'true' to require valid tokens for all requests (recommended)
# Set to 'false' to disable authentication requirement (for testing)
REQUIRE_AUTH=true

# Server Configuration
PORT=4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we’re ready to start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running ToolHive
&lt;/h3&gt;

&lt;p&gt;In our testing, we’re using the already existing Apollo MCP server with no modifications - all the heavy lifting is done by ToolHive. The Apollo MCP server is merely configured to accept the downstream authentication token in the &lt;em&gt;Authorization: Bearer&lt;/em&gt; HTTP header and forward it to the external API.&lt;br&gt;
The MCP server configuration can be found in the &lt;a href="https://github.com/StacklokLabs/apollo-mcp-auth-demo/blob/main/mcp-server-data/apollo-mcp-config.yaml" rel="noopener noreferrer"&gt;mcp-server-data directory&lt;/a&gt; in the demo repository.&lt;/p&gt;

&lt;p&gt;Because the unmodified MCP server also validates the incoming tokens, we need to set the &lt;em&gt;transport.auth.servers&lt;/em&gt; attribute in the config file to the &lt;em&gt;back end&lt;/em&gt; Authorization server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim mcp-server-data/apollo-mcp-config.yaml

...
transport:
  type: sse
  port: 8000
  auth:
    servers:
      - https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can run the server with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv run \
--debug \
--foreground \
--transport streamable-http \
--name apollo \
--target-port 8000 \
--proxy-port 8000 \
--volume $(pwd)/mcp-server-data/apollo-mcp-config.yaml:/config.yaml \
--volume $(pwd)/mcp-server-data:/data \
--oidc-audience mcpserver \
--resource-url http://localhost:8000/mcp \
       --oidc-issuer https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697 \
--oidc-jwks-url https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys \
--token-exchange-audience backend \
--token-exchange-client-id 0oawdgw7krVBSwzIx697 \
--token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 \
--token-exchange-scopes backend-api:read \
--token-exchange-url https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token \
apollo-mcp-server -- /config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s unpack the parameters:&lt;br&gt;
--oidc-audience mcpserver - When the OIDC token from VSCode arrives to toolhive, then toolhive checks if the token’s aud field matches this value and rejects the connection otherwise&lt;/p&gt;

&lt;p&gt;--resource-url &lt;a href="http://localhost:9090/mcp" rel="noopener noreferrer"&gt;http://localhost:9090/mcp&lt;/a&gt; - Setting the resource explicitly helps VSCode discover the proper Protected Resource Metadata Endpoint as per the MCP specification and in effect points VSCode to the Okta instance. Typically not needed in e.g. Kubernetes environments where the service name can be used&lt;/p&gt;

&lt;p&gt;--oidc-issuer &lt;a href="https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697" rel="noopener noreferrer"&gt;https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697&lt;/a&gt; - This is the issuer of the mcpserver Authorization Server (see the first screenshot of the document)&lt;/p&gt;

&lt;p&gt;--oidc-jwks-url &lt;a href="https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys" rel="noopener noreferrer"&gt;https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys&lt;/a&gt; - The JWKS endpoint of the mcpserver Authorization Server&lt;/p&gt;

&lt;p&gt;--token-exchange-audience 'backend' - We want ToolHive to take the incoming tokens and exchange them for tokens with audience of “backend”&lt;/p&gt;

&lt;p&gt;--token-exchange-client-id 0oawdgw7krVBSwzIx697 - The Client ID of the “ToolHive client”, the one who has assigned the token exchange policy to itself&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 - the client secret of the ToolHive client. Outside demos, please use the --token-exchange-client-secret-file switch instead, or the TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET environment variable.

--token-exchange-scopes 'backend-api:read' - The scopes we request for the external token. Must match what’s in the policy.

--token-exchange-url [https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token](https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token) - the token endpoint of the back end Authorization Server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Note that the example above uses &lt;em&gt;thv run&lt;/em&gt;, but it’s equally possible to use the token exchange from thv proxy which can then also provide authentication to the MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv proxy demo-mcp-server \
    --target-uri http://localhost:8091 \
    --port 3000 \
    --remote-auth \
    --remote-auth-client-id 0oawdhc2mlgHOwNvW697 \
    --remote-auth-client-secret Ag0Zj6ALuxxqascP6KJ-CA4uCRcOLmIKtQeR_o3ClGgxMxx0zcgZYYtg-TmHF6U- \
    --remote-auth-issuer https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697 \
    --remote-auth-scopes 'mcp:tools:call,openid,email' \
    --token-exchange-audience 'backend' \
    --token-exchange-client-id 0oawdgw7krVBSwzIx697 \
    --token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 \
    --token-exchange-scopes 'backend-api:read' \
    --token-exchange-url https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authentication from VSCode and putting it all together
&lt;/h3&gt;

&lt;p&gt;Once the server is running, it should automatically appear in the list of the configured MCP servers in VSCode. Clicking Start will prompt authentication against Okta. The first time, you’ll be prompted to enter the client ID and secret as well. Once Okta authenticates, VSCode receives the token, uses it to authenticate to the MCP server (toolhive) which exchanges the token which enables calling the back end API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqacyukmvg7hdiuh9sbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqacyukmvg7hdiuh9sbi.png" alt="VSCode" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Past the initial setup on the IDP side, authentication and authorization to the MCP server fronted by ToolHive and by extension the back end service is seamless and allows partition access to the back end services as well as provides a cleaner audit trail.&lt;/p&gt;

&lt;p&gt;As the last step, we can invoke one of the MCP tools to verify the setup end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0aakq5mtogxz8bdcq0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0aakq5mtogxz8bdcq0d.png" alt="MCP tools" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As seen on the screenshot above, the GetCountry tool of the Apollo server was called and returned a reply! If we check the logs of the API server we ran earlier we also see details of the token that was validated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4frsoeoauh9o04zydipb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4frsoeoauh9o04zydipb.png" alt="Tool usage" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This token has different audience than the one passed to the ToolHive - if you recall the thv run parameters, they specified, through the &lt;em&gt;--oidc-audience&lt;/em&gt; mcpserver argument that the tokens must set the &lt;em&gt;aud&lt;/em&gt; claim to &lt;em&gt;mcpserver&lt;/em&gt; while the token that arrived to the back end API has audience &lt;em&gt;backend&lt;/em&gt;. Looking closely at the issuer, we also see that the token was issued by the back end Authorization Server, while the tokens issued to authenticate to ToolHive were issued by the mcpserver Authorization Server. This shows that the token exchange works correctly. In the next section, we’ll illustrate for completeness’ sake how the tokens look exactly and how the whole flow works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token exchange under the hood
&lt;/h2&gt;

&lt;p&gt;The flow is described in the Mermaid diagram below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89fvfxlk28z6kfz5dohy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89fvfxlk28z6kfz5dohy.png" alt="Diagram" width="512" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client authenticates to the toolhive which exposes the interface and endpoints as the &lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;MCP standard describes&lt;/a&gt;. The toolhive authentication middleware verifies the token was issued by the expected IDP and has the expected audience. After authentication, the token is then passed to the Token Exchange middleware which contacts the IDP and exchanges the token meant for the MCP server for the token meant for the external service.&lt;/p&gt;

&lt;p&gt;The token issued to the client might look like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "iss": https://idp.example.com/oauth2/default",
    "aud": "mcp-server",
    "scp": [
        "backend-mcp:tools:call",
        "backend-mcp:tools:list",
    ],
    "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the exchanged token would have different scopes and a different audience, allowing the MCP server to authenticate to the back end service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "iss": https://idp.example.com/oauth2/default",
    "aud": "backend-server",
    "scp": [
        "backend-api:read",
    ],
    "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exchanged token is then injected into the &lt;em&gt;Authorization: Bearer&lt;/em&gt; HTTP header and passed on to the actual MCP server running under Toolhive. The MCP server can then use the token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and benefits
&lt;/h2&gt;

&lt;p&gt;By leveraging token exchange, ToolHive enables MCP servers to authenticate to third-party APIs in a &lt;strong&gt;secure, efficient, and tenant-aware&lt;/strong&gt; way. MCP servers receive properly scoped, short-lived access tokens instead of embedding long-lived secrets or bespoke authentication logic. Each API call made upstream can be attributed to the &lt;strong&gt;individual user identity&lt;/strong&gt; rather than a generic service account, making audit trails clearer and more meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/tutorials/security/authorization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.okta.com/docs/guides/set-up-token-exchange/main/" rel="noopener noreferrer"&gt;https://developer.okta.com/docs/guides/set-up-token-exchange/main/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Using Token Exchange with ToolHive and Okta for MCP Server to GraphQL Authentication</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Tue, 04 Nov 2025 16:37:21 +0000</pubDate>
      <link>https://forem.com/stacklok/using-token-exchange-with-toolhive-and-okta-for-mcp-server-to-graphql-authentication-12in</link>
      <guid>https://forem.com/stacklok/using-token-exchange-with-toolhive-and-okta-for-mcp-server-to-graphql-authentication-12in</guid>
      <description>&lt;p&gt;This article builds on our &lt;a href="https://dev.to/stacklok/beyond-api-keys-token-exchange-identity-federation-mcp-servers-5dm8"&gt;previous post&lt;/a&gt;, where we introduced the core concepts of token exchange and its role in secure authentication. Here, we delve into a practical application, demonstrating how to leverage Okta and ToolHive to facilitate token exchange for authenticating an MCP server with a GraphQL API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;p&gt;This demo mimics a (hopefully!) real world example where we run an API service and we want to expose it with an MCP server. The back end API requires a token with &lt;em&gt;aud=backend&lt;/em&gt; and &lt;em&gt;scopes=[backend-api:read]&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;"Aud" (audience) in a token specifies the intended recipient of the token, indicating which service or application is meant to consume it. "Scopes" define the specific permissions or access rights granted by the token, detailing what actions the token holder is authorized to perform. Only tokens having the expected audience and the expected scopes authorize the caller to use the service.&lt;/p&gt;

&lt;p&gt;We don’t want to expose the back end service directly to the AI client, but only through the MCP server. We also want to maintain a clean audit trail showing us who accessed what.&lt;/p&gt;

&lt;p&gt;The MCP server requires a token with &lt;em&gt;aud=mcpserver&lt;/em&gt; and &lt;em&gt;scopes=mcp:tools:call&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Both the API service and the MCP server are part of the same Okta realm, but we’ll use different Authorization Servers to ensure that both the token the MCP server receives and the token use different audiences.&lt;/p&gt;

&lt;p&gt;We’ll simulate the whole flow as a developer connecting to this setup by adding the MCP server to VSCode and calling the tools it provides.&lt;/p&gt;

&lt;p&gt;It should be noted that in this example, we’ll be using an &lt;a href="https://www.apollographql.com/docs" rel="noopener noreferrer"&gt;Apollo&lt;/a&gt;-based GraphQL service as the backend API service and the existing &lt;a href="https://www.apollographql.com/docs/apollo-mcp-server" rel="noopener noreferrer"&gt;Apollo MCP server&lt;/a&gt;, but the same setup applies to any kind of API services as long as they both use OAuth tokens from the same realm as the authentication mechanism. &lt;/p&gt;

&lt;p&gt;In order to follow along, you can clone the Apollo GraphQL service from &lt;a href="https://github.com/StacklokLabs/apollo-mcp-auth-demo" rel="noopener noreferrer"&gt;a demo repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okta setup
&lt;/h2&gt;

&lt;p&gt;I’ve used the Okta integrator setup to prepare this demo and therefore the instructions cover the whole setup from the ground up including creating the Authorization Servers. This is likely not needed or needs to be adjusted in a real world environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization Servers
&lt;/h3&gt;

&lt;p&gt;To logically separate the MCP server from the back end API service, we’ll configure two Okta Authorization servers - one for the MCP server and client and the other for the backend server. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyrkj0nwo8nmto911fag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyrkj0nwo8nmto911fag.png" alt="Okta authorization servers" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the Authorization Servers and then the following scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mcpserver AS &lt;em&gt;mcp:tools:call&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;backend AS &lt;em&gt;backend-api:read&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Trust between authorization servers
&lt;/h4&gt;

&lt;p&gt;In order to enable token exchange between two authorization servers - the one that issues tokens for access to the MCP server and the one that issues tokens for accessing the back end, we need to establish trust between the two.&lt;/p&gt;

&lt;p&gt;Go to the back end AS and down at the settings tab, add the mcpserver AS as trusted:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wycf3a7pyhzfjua0719.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wycf3a7pyhzfjua0719.png" alt="Okta trusted server" width="419" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Applications
&lt;/h3&gt;

&lt;p&gt;We’ll set up two Applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;em&gt;VSCode client&lt;/em&gt; to authenticate to the MCP server. We create a client directly to avoid Dynamic Client registration. This will be an OIDC application with a client ID and a secret. It is important to match the Redirect URIs that VSCode uses. Set the Redirect URIs to &lt;a href="http://127.0.0.1:33418" rel="noopener noreferrer"&gt;http://127.0.0.1:33418&lt;/a&gt; and &lt;a href="https://vscode.dev/redirect" rel="noopener noreferrer"&gt;https://vscode.dev/redirect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;toolhive client&lt;/em&gt; that will perform the Token Exchange. This is an API Services type in Okta lingo. To create the application, go to:&lt;/li&gt;
&lt;li&gt;Applications -&amp;gt; Create App Integration and select API Services&lt;/li&gt;
&lt;li&gt;Name your application&lt;/li&gt;
&lt;li&gt;In the application page, navigate to the General Settings page and uncheck the “Require Demonstrating Proof of Possession” header as this is not yet supported by ToolHive&lt;/li&gt;
&lt;li&gt;Check the Token Exchange grant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfkajllk1la4o93y0vve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfkajllk1la4o93y0vve.png" alt="Token exchange grant" width="478" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Policies
&lt;/h3&gt;

&lt;p&gt;In order for applications to authenticate, we need to include them in policies, otherwise Okta will not issue tokens to the clients. We’ll define two policies: One that allows the MCP Client (VSCode) to request tokens with &lt;em&gt;mcp:tools:call&lt;/em&gt; and another one that allows the token exchange by the ToolHive process.&lt;/p&gt;

&lt;h4&gt;
  
  
  MCP client to MCP server
&lt;/h4&gt;

&lt;p&gt;This policy is to be defined on the mcpserver AS side. Select “Add New Access Policy”, then “Assign to the following Clients” and select the VSCode client. When the policy is created, click “Add Rule” in the policy and in the “And the following scopes” section add both the “OpenID Connect” scopes and the &lt;em&gt;mcp:tools:call&lt;/em&gt; scopes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22mxe559jdbd901o637c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22mxe559jdbd901o637c.png" alt="Scopes" width="512" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  MCP server token exchange
&lt;/h4&gt;

&lt;p&gt;This policy is to be defined on the back end AS side. Select “Add New Access Policy”, then “Assign to the following Clients” and select the ToolHive client. When adding the rule, don’t forget to unroll “Advanced” under the “If Grant Type Is” section and add Token Exchange. Add “&lt;em&gt;backend-api:read&lt;/em&gt;” to the scopes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed28ypvi5nioio3fpu5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed28ypvi5nioio3fpu5z.png" alt="Scopes" width="512" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8uarrmw0krmln9zc3vi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8uarrmw0krmln9zc3vi.png" alt="Token exchange" width="512" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the GraphQL server
&lt;/h3&gt;

&lt;p&gt;Let’s clone our server locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/StacklokLabs/apollo-mcp-auth-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s configure the IDP settings in the .env file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp .env.example .env
vim .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using my Okta integrator account, the .env file looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Okta Configuration
# Your Okta domain (e.g., dev-123456.okta.com)
OKTA_DOMAIN=integrator-3683736.okta.com

# Your Okta issuer URL (authorization server)
# For default authorization server: https://your-domain.okta.com/oauth2/default
# For custom authorization server: https://your-domain.okta.com/oauth2/{authServerId}
OKTA_ISSUER=https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697

# JWT Validation Configuration
# Expected audience in JWT tokens (space-separated if multiple)
OKTA_AUDIENCE=backend
# Required scopes in JWT tokens (space-separated)
REQUIRED_SCOPES=backend-api:read

# Authentication Configuration
# Set to 'true' to require valid tokens for all requests (recommended)
# Set to 'false' to disable authentication requirement (for testing)
REQUIRE_AUTH=true

# Server Configuration
PORT=4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we’re ready to start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running ToolHive
&lt;/h3&gt;

&lt;p&gt;In our testing, we’re using the already existing Apollo MCP server with no modifications - all the heavy lifting is done by ToolHive. The Apollo MCP server is merely configured to accept the downstream authentication token in the &lt;em&gt;Authorization: Bearer&lt;/em&gt; HTTP header and forward it to the external API.&lt;br&gt;
The MCP server configuration can be found in the &lt;a href="https://github.com/StacklokLabs/apollo-mcp-auth-demo/blob/main/mcp-server-data/apollo-mcp-config.yaml" rel="noopener noreferrer"&gt;mcp-server-data directory&lt;/a&gt; in the demo repository.&lt;/p&gt;

&lt;p&gt;Because the unmodified MCP server also validates the incoming tokens, we need to set the &lt;em&gt;transport.auth.servers&lt;/em&gt; attribute in the config file to the &lt;em&gt;back end&lt;/em&gt; Authorization server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim mcp-server-data/apollo-mcp-config.yaml

...
transport:
  type: sse
  port: 8000
  auth:
    servers:
      - https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can run the server with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv run \
--debug \
--foreground \
--transport streamable-http \
--name apollo \
--target-port 8000 \
--proxy-port 8000 \
--volume $(pwd)/mcp-server-data/apollo-mcp-config.yaml:/config.yaml \
--volume $(pwd)/mcp-server-data:/data \
--oidc-audience mcpserver \
--resource-url http://localhost:8000/mcp \
       --oidc-issuer https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697 \
--oidc-jwks-url https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys \
--token-exchange-audience backend \
--token-exchange-client-id 0oawdgw7krVBSwzIx697 \
--token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 \
--token-exchange-scopes backend-api:read \
--token-exchange-url https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token \
apollo-mcp-server -- /config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s unpack the parameters:&lt;br&gt;
--oidc-audience mcpserver - When the OIDC token from VSCode arrives to toolhive, then toolhive checks if the token’s aud field matches this value and rejects the connection otherwise&lt;/p&gt;

&lt;p&gt;--resource-url &lt;a href="http://localhost:9090/mcp" rel="noopener noreferrer"&gt;http://localhost:9090/mcp&lt;/a&gt; - Setting the resource explicitly helps VSCode discover the proper Protected Resource Metadata Endpoint as per the MCP specification and in effect points VSCode to the Okta instance. Typically not needed in e.g. Kubernetes environments where the service name can be used&lt;/p&gt;

&lt;p&gt;--oidc-issuer &lt;a href="https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697" rel="noopener noreferrer"&gt;https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697&lt;/a&gt; - This is the issuer of the mcpserver Authorization Server (see the first screenshot of the document)&lt;/p&gt;

&lt;p&gt;--oidc-jwks-url &lt;a href="https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys" rel="noopener noreferrer"&gt;https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697/v1/keys&lt;/a&gt; - The JWKS endpoint of the mcpserver Authorization Server&lt;/p&gt;

&lt;p&gt;--token-exchange-audience 'backend' - We want ToolHive to take the incoming tokens and exchange them for tokens with audience of “backend”&lt;/p&gt;

&lt;p&gt;--token-exchange-client-id 0oawdgw7krVBSwzIx697 - The Client ID of the “ToolHive client”, the one who has assigned the token exchange policy to itself&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 - the client secret of the ToolHive client. Outside demos, please use the --token-exchange-client-secret-file switch instead, or the TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET environment variable.

--token-exchange-scopes 'backend-api:read' - The scopes we request for the external token. Must match what’s in the policy.

--token-exchange-url [https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token](https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token) - the token endpoint of the back end Authorization Server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Note that the example above uses &lt;em&gt;thv run&lt;/em&gt;, but it’s equally possible to use the token exchange from thv proxy which can then also provide authentication to the MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv proxy demo-mcp-server \
    --target-uri http://localhost:8091 \
    --port 3000 \
    --remote-auth \
    --remote-auth-client-id 0oawdhc2mlgHOwNvW697 \
    --remote-auth-client-secret Ag0Zj6ALuxxqascP6KJ-CA4uCRcOLmIKtQeR_o3ClGgxMxx0zcgZYYtg-TmHF6U- \
    --remote-auth-issuer https://integrator-3683736.okta.com/oauth2/ausw8f1ut6X0WMjZN697 \
    --remote-auth-scopes 'mcp:tools:call,openid,email' \
    --token-exchange-audience 'backend' \
    --token-exchange-client-id 0oawdgw7krVBSwzIx697 \
    --token-exchange-client-secret O2zqVb-evhKgfBOD-PRVDs5HFyCXAnRZAwxAtQOH9oGt72aBrLBiwEVlyyTengj9 \
    --token-exchange-scopes 'backend-api:read' \
    --token-exchange-url https://integrator-3683736.okta.com/oauth2/auswdh3wurjeJ62La697/v1/token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authentication from VSCode and putting it all together
&lt;/h3&gt;

&lt;p&gt;Once the server is running, it should automatically appear in the list of the configured MCP servers in VSCode. Clicking Start will prompt authentication against Okta. The first time, you’ll be prompted to enter the client ID and secret as well. Once Okta authenticates, VSCode receives the token, uses it to authenticate to the MCP server (toolhive) which exchanges the token which enables calling the back end API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqacyukmvg7hdiuh9sbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqacyukmvg7hdiuh9sbi.png" alt="VSCode" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Past the initial setup on the IDP side, authentication and authorization to the MCP server fronted by ToolHive and by extension the back end service is seamless and allows partition access to the back end services as well as provides a cleaner audit trail.&lt;/p&gt;

&lt;p&gt;As the last step, we can invoke one of the MCP tools to verify the setup end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0aakq5mtogxz8bdcq0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn0aakq5mtogxz8bdcq0d.png" alt="MCP tools" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As seen on the screenshot above, the GetCountry tool of the Apollo server was called and returned a reply! If we check the logs of the API server we ran earlier we also see details of the token that was validated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4frsoeoauh9o04zydipb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4frsoeoauh9o04zydipb.png" alt="Tool usage" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This token has different audience than the one passed to the ToolHive - if you recall the thv run parameters, they specified, through the &lt;em&gt;--oidc-audience&lt;/em&gt; mcpserver argument that the tokens must set the &lt;em&gt;aud&lt;/em&gt; claim to &lt;em&gt;mcpserver&lt;/em&gt; while the token that arrived to the back end API has audience &lt;em&gt;backend&lt;/em&gt;. Looking closely at the issuer, we also see that the token was issued by the back end Authorization Server, while the tokens issued to authenticate to ToolHive were issued by the mcpserver Authorization Server. This shows that the token exchange works correctly. In the next section, we’ll illustrate for completeness’ sake how the tokens look exactly and how the whole flow works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token exchange under the hood
&lt;/h2&gt;

&lt;p&gt;The flow is described in the Mermaid diagram below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89fvfxlk28z6kfz5dohy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89fvfxlk28z6kfz5dohy.png" alt="Diagram" width="512" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client authenticates to the toolhive which exposes the interface and endpoints as the &lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;MCP standard describes&lt;/a&gt;. The toolhive authentication middleware verifies the token was issued by the expected IDP and has the expected audience. After authentication, the token is then passed to the Token Exchange middleware which contacts the IDP and exchanges the token meant for the MCP server for the token meant for the external service.&lt;/p&gt;

&lt;p&gt;The token issued to the client might look like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "iss": https://idp.example.com/oauth2/default",
    "aud": "mcp-server",
    "scp": [
        "backend-mcp:tools:call",
        "backend-mcp:tools:list",
    ],
    "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the exchanged token would have different scopes and a different audience, allowing the MCP server to authenticate to the back end service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "iss": https://idp.example.com/oauth2/default",
    "aud": "backend-server",
    "scp": [
        "backend-api:read",
    ],
    "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exchanged token is then injected into the &lt;em&gt;Authorization: Bearer&lt;/em&gt; HTTP header and passed on to the actual MCP server running under Toolhive. The MCP server can then use the token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and benefits
&lt;/h2&gt;

&lt;p&gt;By leveraging token exchange, ToolHive enables MCP servers to authenticate to third-party APIs in a &lt;strong&gt;secure, efficient, and tenant-aware&lt;/strong&gt; way. MCP servers receive properly scoped, short-lived access tokens instead of embedding long-lived secrets or bespoke authentication logic. Each API call made upstream can be attributed to the &lt;strong&gt;individual user identity&lt;/strong&gt; rather than a generic service account, making audit trails clearer and more meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/tutorials/security/authorization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.okta.com/docs/guides/set-up-token-exchange/main/" rel="noopener noreferrer"&gt;https://developer.okta.com/docs/guides/set-up-token-exchange/main/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Beyond API Keys: Token Exchange, Identity Federation &amp; MCP Servers</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Thu, 30 Oct 2025 11:04:03 +0000</pubDate>
      <link>https://forem.com/stacklok/beyond-api-keys-token-exchange-identity-federation-mcp-servers-5dm8</link>
      <guid>https://forem.com/stacklok/beyond-api-keys-token-exchange-identity-federation-mcp-servers-5dm8</guid>
      <description>&lt;p&gt;Modern backend systems—especially in the era of AI agents, MCP servers, and multi-cloud architectures—are evolving far beyond static credentials and monolithic identity models. In this post we explore the architecture of token exchange, identity federation, and how a system like &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; enables secure deployment of MCP servers in this world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legacy problem: static credentials
&lt;/h2&gt;

&lt;p&gt;The MCP authorization specification focuses on how to authorize access to the MCP server itself. It doesn't specify how an MCP server should authenticate with the server it's connecting to. This leaves MCP server creators without clear guidance.&lt;/p&gt;

&lt;p&gt;In many deployments of MCP (Model Context Protocol) servers and tooling services today, developers still default to patterns like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A service-account JSON key or a long-lived API key embedded in configuration.&lt;/li&gt;
&lt;li&gt;All calls executed under a single “shared identity” with elevated permissions.&lt;/li&gt;
&lt;li&gt;If the key is compromised, the impact spans many users or tenants; rotating or tracking the key is operationally heavy.&lt;/li&gt;
&lt;li&gt;Least-privilege is often compromised because the shared identity needs broad access to avoid blocking tool invocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach doesn’t align with how modern identity systems, federated services and cloud tools are designed. It’s less secure, harder to govern, and doesn’t scale across users or multi‐tenant environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step up: Short-lived tokens via an IdP
&lt;/h2&gt;

&lt;p&gt;A much better pattern emerges when you shift to short-lived tokens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user (or service) authenticates via an Identity Provider (IdP) — for example, Okta or Azure AD.&lt;/li&gt;
&lt;li&gt;They receive a short-lived token (OIDC ID token or OAuth access token) that's scoped to their identity and minimal permissions.&lt;/li&gt;
&lt;li&gt;This token is used to authenticate to the MCP server (with the help of ToolHive), which validates it and establishes the user's identity.&lt;/li&gt;
&lt;li&gt;Toolhive then acquires a separate token for the downstream backend API—either through token exchange (if using the same IdP) or federation (if crossing identity domains).&lt;/li&gt;
&lt;li&gt;Your MCP server receives this backend-scoped token and uses it when calling downstream services or tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because tokens are scoped, time-limited, and mapped to a specific user context, you get better auditability, enforce least-privilege, and eliminate static credentials. Next, we’ll show you how to ensure that your MCP server always has the right credentials for its backend API without embedding secrets or handling complex auth flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Exchange &amp;amp; Federation: crossing trust-boundaries
&lt;/h2&gt;

&lt;p&gt;Token exchange refers to the process where one security token (issued by one identity domain) is presented to a “Security Token Service” (STS) or similar endpoint, and in return you receive a new token valid for another domain, audience, or scope.&lt;br&gt;
The standard for this is &lt;a href="https://www.rfc-editor.org/rfc/rfc8693.html" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt; (OAuth 2.0 Token Exchange) which lets you request a new token via a grant like &lt;em&gt;urn:ietf:params:oauth:grant-type:token-exchange&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use-cases for token exchange include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A token issued by your internal IdP being exchanged for a token valid for a cloud provider’s API.&lt;/li&gt;
&lt;li&gt;A token from one IdP being reused to obtain tokens in another trust domain without forcing the user to log in again.&lt;/li&gt;
&lt;li&gt;A service acting on behalf of a user, exchanging its own token for one with narrower scopes or different audiences.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Two common scenarios
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A) The downstream service uses the same IdP as the MCP server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this case your identity provider (IdP) issues tokens for both the MCP server and the downstream resources. No cross-domain trust is needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authenticates via IdP → obtains a token for the MCP server.&lt;/li&gt;
&lt;li&gt;ToolHive validates the token and performs access control checks.&lt;/li&gt;
&lt;li&gt;ToolHive exchanges that token with the same IdP for a new token with the downstream service's audience and scopes.&lt;/li&gt;
&lt;li&gt;MCP server receives this exchanged token and uses it to call the downstream service.
​​- Simpler, fewer moving parts, since the exchange happens within the same IdP ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F80v5zykok3bry9locp99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F80v5zykok3bry9locp99.png" alt="Token exchange with single IDP" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The token issued to the client might look like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   "iss": https://idp.example.com/oauth2/default",
   "aud": "**mcp-server**",
   "scp": [
     "**backend-mcp:tools:call**",
     "**backend-mcp:tools:list**",
   ],
   "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the exchanged token would have different scopes and a different audience, allowing the MCP server to authenticate to the back end service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "iss": https://idp.example.com/oauth2/default",
    "aud": "**backend-server**",
    "scp": [
        "**backend-api:read**",
    ],
    "sub": "user@example.com",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B) The downstream service uses a different IdP and you rely on federation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here you have two distinct identity/trust domains: one used by the MCP server (or its IdP) and another used by the back end resource. Instead of issuing separate credentials or having users login twice, you rely on federation and token exchange.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authenticates via IdP A → receives a token for domain A that is presented to ToolHive&lt;/li&gt;
&lt;li&gt;ToolHive validates the token and performs access control checks.&lt;/li&gt;
&lt;li&gt;ToolHive presents the token to an STS or federation service (e.g., Google Cloud STS) → obtains a federated token valid for domain B (cloud provider).&lt;/li&gt;
&lt;li&gt;Downstream service validates the token from domain B and executes requests under that identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach enables your system to be IdP-agnostic and cloud-agnostic: authenticate with any IdP, then federate into any trust-configured domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgf5czj7ibj0nmg8trmra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgf5czj7ibj0nmg8trmra.png" alt="Flow diagram about federation" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The token issued to the client might look like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "iss": "**https://idp.example.com/oauth2/default**",
  "aud": "**mcp-server**",
  "sub": "user@example.com",
  "email": "user@example.com",
  "scp": [
    "**mcp:tools:call**",
    "**mcp:tools:list**"
  ],
  "exp": 1729641600,
  "iat": 1729638000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exchanged federated access token would have a different issuer, audience, and scopes, allowing the MCP server to authenticate to the upstream service as the federated user identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "iss": "**https://sts.googleapis.com**",
  "aud": "**https://bigquery.googleapis.com/**",
  "sub": "**principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/user@example.com**",
  "email": "user@example.com",
  "scp": [
    "**https://www.googleapis.com/auth/bigquery**",
  ],
  "exp": 1729641600,
  "iat": 1729638000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters for MCP servers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MCP servers are often deployed to call different services on behalf of users. If they rely on static credentials or simplistic “shared identity” models, you lose user-level attribution, least-privilege control, and auditability.&lt;/li&gt;
&lt;li&gt;By using token exchange + federation, you allow your MCP server to operate under the right identity context, even when the target service sits in a different trust domain.&lt;/li&gt;
&lt;li&gt;It also lets you design your architecture so the authentication piece (login, token issuance) is decoupled from the MCP server logic — the server can remain auth-agnostic and medium-agnostic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where ToolHive fits
&lt;/h2&gt;

&lt;p&gt;ToolHive simplifies deployment of MCP servers by handling the operational and security heavy-lifting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You run your MCP servers in containers with minimal permissions and network access — ToolHive manages that.&lt;/li&gt;
&lt;li&gt;ToolHive acts as a gateway: it verifies the user's token (via your IdP), enforces access policies, then acquires the appropriate backend token—either through exchange or federation—before passing that to your MCP server.&lt;/li&gt;
&lt;li&gt;This separation means your MCP server remains auth-agnostic — ToolHive handles authN/authZ and you plug in any IdP or downstream STS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This blog post is the first in a series&lt;/strong&gt;. Over the coming posts we’ll dive into a set of &lt;strong&gt;practical examples using ToolHive&lt;/strong&gt; — showing how to wire up different IdPs, federate into different clouds, run MCP servers securely, and deal with real-world edge cases.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: ToolHive is an open source project, and we encourage you to download it (from &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;toolhive.dev&lt;/a&gt;) and start using it. We value your feedback and would love to engage with you via our &lt;a href="https://github.com/stacklok/toolhive" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; and/or &lt;a href="https://discord.gg/stacklok" rel="noopener noreferrer"&gt;Discord channel&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Exposing a Kubernetes-Hosted MCP Server with ToolHive + ngrok (with Basic Auth)</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Wed, 17 Sep 2025 15:17:38 +0000</pubDate>
      <link>https://forem.com/stacklok/exposing-a-kubernetes-hosted-mcp-server-with-toolhive-ngrok-with-basic-auth-23kn</link>
      <guid>https://forem.com/stacklok/exposing-a-kubernetes-hosted-mcp-server-with-toolhive-ngrok-with-basic-auth-23kn</guid>
      <description>&lt;p&gt;In the previous &lt;a href="https://dev.to/stacklok/how-to-safely-expose-your-mcp-servers-externally-using-ngrok-and-toolhive-53b7"&gt;post&lt;/a&gt;, we tunneled a &lt;strong&gt;local&lt;/strong&gt; MCP server with &lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; to expose internal services externally (for testing and integration, demo access, branch office access and other scenarios). Now let’s do the same for a &lt;strong&gt;Kubernetes-hosted&lt;/strong&gt; workload managed by &lt;a href="https://toolhive.dev/" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt;. This is very much a production scenario in which exposed MCP servers are also exposed via Kubernetes clusters; but with ToolHive and ngrok, we can keep the approach simple. Once you’ve got ToolHive and ngrok up-and-running, just follow the steps below:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Deploy ToolHive to your cluster, then the fetch MCP server
&lt;/h2&gt;

&lt;p&gt;Follow the ToolHive &lt;a href="https://docs.stacklok.com/toolhive/tutorials/quickstart-k8s" rel="noopener noreferrer"&gt;Kubernetes Operator quickstart&lt;/a&gt; to install the operator and deploy an MCP server in your cluster (I’m using the &lt;em&gt;fetch&lt;/em&gt; server here). The operator turns MCP servers into first-class Kubernetes resources you can manage declaratively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/stacklok/toolhive/refs/heads/main/examples/operator/mcp-servers/mcpserver_fetch.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After applying your manifests/CRs, you’ll see Services like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get service -n toolhive-system
# NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
# mcp-fetch-headless   ClusterIP   None            &amp;lt;none&amp;gt;        8080/TCP   12m
# mcp-fetch-proxy      ClusterIP   10.96.166.106   &amp;lt;none&amp;gt;        8080/TCP   12m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;ClusterIP&lt;/strong&gt; Services, which are intentionally &lt;strong&gt;in-cluster only&lt;/strong&gt; (no host access yet). We’ll bridge them to the host next.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Port-forward the Service to your laptop
&lt;/h2&gt;

&lt;p&gt;Use &lt;em&gt;kubectl port-forward&lt;/em&gt; to map the Service’s port to &lt;em&gt;localhost:8080&lt;/em&gt; so you can reach it from your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n toolhive-system port-forward svc/mcp-fetch-proxy 8080:8080

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt; is a portal to the in-cluster Service.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add a simple ngrok Traffic Policy (HTTP Basic Auth)
&lt;/h2&gt;

&lt;p&gt;Before we open this to the internet, let’s require a username/password via ngrok &lt;strong&gt;Traffic Policy&lt;/strong&gt;. Save a policy file like &lt;em&gt;/tmp/policy.yaml&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on_http_request:
  - actions:
      - type: basic-auth
        config:
          credentials:
            - stacklok:p4ssw0rd

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ngrok’s Basic Auth policy validates the &lt;em&gt;Authorization: Basic …&lt;/em&gt; header, returning &lt;strong&gt;200 OK&lt;/strong&gt; when credentials match, and &lt;strong&gt;401 Unauthorized&lt;/strong&gt; otherwise&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: &lt;code&gt;echo -n 'stacklok:p4ssw0rd' | base64&lt;/code&gt; helps you generate the header value locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Launch the tunnel with ToolHive’s proxy
&lt;/h2&gt;

&lt;p&gt;With the Service forwarded to &lt;em&gt;127.0.0.1:8080&lt;/em&gt;, start a ToolHive tunnel pointing at that local address, telling ToolHive to use ngrok and your policy file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thv proxy tunnel http://127.0.0.1:8080 test \
  --tunnel-provider ngrok \
  --provider-args '{"auth-token":"${NGROK_TOKEN}","traffic-policy-file":"/tmp/policy.yaml"}'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ToolHive will bring up an ngrok HTTPS endpoint and print the public URL for the &lt;em&gt;fetch&lt;/em&gt; MCP server — something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"fetch": {
  "url": "https://bf18062fef8a.ngrok-free.app/mcp",
  "description": "Fetch MCP server for testing",
  "headers": {
    "Authorization": "Basic c3RhY2tsb2s6cDRzc3cwcmQ="
  },
  "type": "http"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send requests with the &lt;strong&gt;Authorization&lt;/strong&gt; header and you’ll get through; omit it and you’ll see a &lt;strong&gt;401&lt;/strong&gt; by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarizing the benefits of this approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes-native management:&lt;/strong&gt; ToolHive’s operator defines and manages MCP servers as Kubernetes resources, which is great for multi-user and production workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe local bridge:&lt;/strong&gt; &lt;code&gt;kubectl port-forward&lt;/code&gt; exposes the internal Service to your host without changing cluster networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardened public edge:&lt;/strong&gt; ngrok’s Traffic Policy adds Basic Auth at the edge so your tunnel isn’t wide open during tests/demos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these few steps, you’ve taken a &lt;strong&gt;Kubernetes-hosted&lt;/strong&gt; MCP server, bridged it to your localhost safely, and published it behind a &lt;strong&gt;secure, temporary ngrok URL&lt;/strong&gt;. This is perfect for quick external tests, demos, or sharing an endpoint without touching production.&lt;/p&gt;

&lt;p&gt;We’re excited about the integration of ToolHive and ngrok and how it quickly and elegantly solves a problem that more enterprises will encounter as they adopt MCP. If you have questions or ideas, we’d love to hear from you. Please checkout &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; and &lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;, and connect with us on &lt;a href="https://discord.gg/stacklok" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How-to Safely Expose your MCP Servers Externally Using ngrok and ToolHive</title>
      <dc:creator>Yolanda Robla Mota</dc:creator>
      <pubDate>Tue, 09 Sep 2025 14:10:23 +0000</pubDate>
      <link>https://forem.com/stacklok/how-to-safely-expose-your-mcp-servers-externally-using-ngrok-and-toolhive-53b7</link>
      <guid>https://forem.com/stacklok/how-to-safely-expose-your-mcp-servers-externally-using-ngrok-and-toolhive-53b7</guid>
      <description>&lt;p&gt;As you make increasing use of Model Context Protocol (&lt;em&gt;MCP&lt;/em&gt;) servers, you’re going to find yourself in a situation where you need to expose these endpoints externally. For example, you may need to expose servers to a partner or customer for testing and integration. Perhaps your organization has a branch office without direct network access but the same need to reach MCP servers. Or, your product may offer MCP ‘tools-as-a-service’ to clients that live outside your VPC. &lt;br&gt;
There’s a quick, simple and safe way to expose MCP servers when they’re managed by &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; and integrated with &lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;.  Below we’ll show you how you can do it using ToolHive's &lt;code&gt;proxy tunnel&lt;/code&gt; command. But, first, a quick description of ToolHive and ngrok for those new to these solutions.&lt;/p&gt;
&lt;h2&gt;
  
  
  ToolHive: The MCP Engine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; is your starting point for running MCP in production. It handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server Lifecycle: Starting, stopping, and managing MCP server instances.&lt;/li&gt;
&lt;li&gt;Transport Methods: Supporting multiple communication protocols (stdio, SSE, streamable-http).&lt;/li&gt;
&lt;li&gt;Security: Managing secrets, permissions, and isolation.&lt;/li&gt;
&lt;li&gt;Discovery: Providing a registry of available MCP servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  ngrok: The API Gateway
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; is a flexible API gateway that provides instant and secure access, anywhere. It handles: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identity and access: Supporting OIDC and OAuth2 and providing tenant-aware RBAC&lt;/li&gt;
&lt;li&gt;Secure tunnel: Handling HTTPS with secure, automatically generated public URLs&lt;/li&gt;
&lt;li&gt;Safety &amp;amp; governance: Establishing rate limits and managing blast radius&lt;/li&gt;
&lt;li&gt;Observability: Providing logs and audit trails&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Getting started:
&lt;/h2&gt;

&lt;p&gt;Before diving in, you’ll need to address a few easy prerequisites:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create an ngrok account&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visit the ngrok website and sign up for a free (or paid) account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Obtain your ngrok auth token&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After logging in, you'll find your authentication token in the ngrok dashboard—often under &lt;em&gt;Auth&lt;/em&gt; or &lt;em&gt;Setup&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Copy that token (here represented as &lt;code&gt;xxx&lt;/code&gt; in examples).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Enable fixed or custom domains (optional)
&lt;/h2&gt;

&lt;p&gt;Set up a permanent, branded domain (e.g. your-app.ngrok.io or a custom domain like api.yourcompany.com) instead of a random address by claiming your free static domain at &lt;a href="//dashboard.ngrok.com/domains"&gt;dashboard.ngrok.com/domains&lt;/a&gt;. &lt;/p&gt;
&lt;h2&gt;
  
  
  Exposing MCP server endpoints
&lt;/h2&gt;

&lt;p&gt;As we work through this example, imagine you’ve got an OSV MCP server that you want to expose externally, so that other users can test your integration. You set-up and are managing that OSV MCP server using ToolHive with internal workload listening on localhost.&lt;/p&gt;

&lt;p&gt;Your first command would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export NGROK_TOKEN=&amp;lt;your_ngrok_token&amp;gt;
export NGROK_URL=&amp;lt;your_ngrok_url&amp;gt;

thv proxy tunnel osv test \
  --tunnel-provider ngrok \
  --provider-args "{
    \"auth-token\": \"${NGROK_TOKEN}\",
    \"url\": \"${NGROK_URL}\"
  }"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command includes these actions: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thv proxy tunnel osv test spins up a tunnel for the ToolHive workload named osv (in a test context).&lt;/li&gt;
&lt;li&gt;--tunnel-provider ngrok tells ToolHive to use ngrok as the tunneling mechanism.&lt;/li&gt;
&lt;li&gt;--provider-args passes any needed parameters for ngrok, such as authentication credentials so the tunnel will establish properly under your account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i4zjnv0trphy90z6xpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i4zjnv0trphy90z6xpp.png" alt=" " width="800" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result is an endpoint in ngrok with all the settings configured:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0h3uytq8ugbtsxbyx7yw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0h3uytq8ugbtsxbyx7yw.png" alt=" " width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the command runs successfully, you’ll get a public HTTPS URL that you can use and integrate into your tools. In our example, that URL looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"osv": {
  "url": "https://ricarda-presuggestive-archaically.ngrok-free.app",
  "description": "OSV MCP server for testing"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that means that the local OSV MCP server, which was accessible only on localhost, is now reachable externally, and can be used by other users to test your integration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftt2ttsvam5aazlijbtnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftt2ttsvam5aazlijbtnz.png" alt=" " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarizing the benefits of this approach
&lt;/h2&gt;

&lt;p&gt;With a simple (single) command, you’ve set-up an instant, public URL with no DNS changes or firewall configuration. It’s a secure HTTPS endpoint that’s lightweight and temporary, so it’s ideal for short-term testing, demos or collaborating with remote customers and teammates. &lt;/p&gt;

&lt;p&gt;We’re excited about the integration of ToolHive and ngrok and how it quickly and elegantly solves a problem that more enterprises will encounter as they adopt MCP. If you have questions or ideas, we’d love to hear from you. Please checkout &lt;a href="https://toolhive.dev" rel="noopener noreferrer"&gt;ToolHive&lt;/a&gt; and &lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;, and connect with us on &lt;a href="https://discord.gg/stacklok" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
