Skip to content

Commit 91f45af

Browse files
renovate[bot]github-actions[bot]danbarrclaude
authored
Update stacklok/toolhive to v0.32.0 (#993)
* Update stacklok/toolhive to v0.32.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Refresh reference assets for toolhive v0.32.0 * Document toolhive v0.32.0 user-facing changes Cover the new --allowed-origins flag, XAA outgoing-auth strategy, OpenAI-compatible embedding provider, EmbeddedAuthServer insecureAllowHTTP, and MCPRemoteProxy OIDC CA bundle behavior. * Smooth out two spaced-hyphen separators in v0.32.0 docs * Drop OIDC CA bundle admonition in remote proxy docs The admonition described expected behavior of caBundleRef (now fixed upstream in a bugfix), reading like a new feature. The actionable guidance to set caBundleRef already lives in the Production security warning, so the callout was redundant. Co-authored-by: Dan Barr <danbarr@users.noreply.github.com> * Tighten Origin headers section in run-mcp-servers Merge redundant non-loopback explanations into one paragraph and fix an ambiguous loopback-address parenthetical. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Drop unsupported XAA example, fix optimizer admonition XAA isn't wired into the Kubernetes operator: the converter discards spec.config.outgoingAuth, and MCPExternalAuthConfig's type enum doesn't include xaa, so the CRD example in authentication.mdx can't work today. Remove it rather than document a feature that silently falls back to unauthenticated. Also scope the "EmbeddingServer is always required" admonition in optimizer.mdx to the default tei provider; it doesn't apply when embeddingService is set directly, e.g. via the OpenAI-compatible provider added in this release. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Dan Barr <danbarr@users.noreply.github.com> Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 493dc42 commit 91f45af

11 files changed

Lines changed: 436 additions & 22 deletions

File tree

.github/upstream-projects.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ projects:
4444

4545
- id: toolhive
4646
repo: stacklok/toolhive
47-
version: v0.31.0
47+
version: v0.32.0
4848
# toolhive is a monorepo covering the CLI, the Kubernetes
4949
# operator, and the vMCP gateway. It also introduces cross-
5050
# cutting features that land in concepts/, integrations/,

docs/toolhive/guides-cli/run-mcp-servers.mdx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,42 @@ thv run --transport streamable-http --target-port <PORT_NUMBER> <SERVER> -- http
349349
Check your MCP server's documentation for the required transport and port
350350
configuration.
351351

352+
### Restrict browser Origin headers
353+
354+
ToolHive validates the HTTP `Origin` header on inbound proxy requests to protect
355+
browser-based clients against DNS-rebinding attacks, per the
356+
[MCP 2025-11-25 specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#security-warning).
357+
Requests without an `Origin` header (such as IDE clients, CLI bridges, and
358+
SDK-based MCP clients) pass through unchanged; only browser cross-origin
359+
requests are subject to the check.
360+
361+
When ToolHive binds to a loopback address (the default is `127.0.0.1`, but
362+
`localhost` and `[::1]` also count), it derives a loopback-only allowlist
363+
automatically and no configuration is required.
364+
365+
When you bind to a non-loopback address with `--host` and a browser client
366+
connects from a different origin, pass `--allowed-origins` to enable the check.
367+
The flag is repeatable, matches each value exactly on scheme, host, and port,
368+
and is also accepted by `thv proxy`. Without it, ToolHive logs a warning and
369+
disables the check entirely, which isn't recommended for public binds:
370+
371+
```bash
372+
thv run --transport streamable-http \
373+
--host 0.0.0.0 \
374+
--allowed-origins https://my-web-app.example.com \
375+
<SERVER>
376+
```
377+
378+
:::info[Legacy SSE transport CORS]
379+
380+
As of v0.32.0, the legacy SSE transport no longer sends a wildcard
381+
`Access-Control-Allow-Origin: *` header. Browser clients on a non-loopback
382+
origin that used to rely on the wildcard must now be added to the
383+
`--allowed-origins` allowlist, or migrated off SSE to the streamable-HTTP
384+
transport.
385+
386+
:::
387+
352388
### Add a custom CA certificate
353389

354390
In corporate environments with TLS inspection or custom certificate authorities,

docs/toolhive/guides-vmcp/authentication.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,16 @@ for key generation steps.
513513

514514
:::
515515

516+
The issuer URL must use the `https://` scheme. The single exception is
517+
`localhost`, which can use `http://` for local development. For in-cluster
518+
deployments where traffic between the embedded auth server and other pods stays
519+
on a trusted network (for example, an in-cluster service mesh), you can opt in
520+
to an `http://` issuer on a non-localhost host by setting
521+
`insecureAllowHTTP: true`. The VirtualMCPServer controller rejects this
522+
combination at reconcile time with `AuthServerConfigValidated=False` if the flag
523+
is unset, so misconfiguration surfaces on the resource rather than crashing the
524+
pod at startup. Never set this for issuers reachable outside the cluster.
525+
516526
If the browser-facing authorization endpoint needs to be on a different host
517527
than the issuer (for example, behind an ingress that rewrites paths), set
518528
`authorizationEndpointBaseUrl` to override the `authorization_endpoint` in the

docs/toolhive/guides-vmcp/local-cli.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ For Tier 2, ToolHive starts and stops a HuggingFace Text Embeddings Inference
240240
(TEI) container named `thv-embedding-<hash>` automatically. Customize the model
241241
and image with `--embedding-model` and `--embedding-image`.
242242

243+
For Tier 3, you can point at any HuggingFace TEI server or at an
244+
OpenAI-compatible `/embeddings` endpoint (OpenAI, Azure OpenAI, or another
245+
compatible gateway). Set `embeddingProvider: openai` and `embeddingModel`
246+
alongside `embeddingService`, and supply the API key via the `OPENAI_API_KEY`
247+
environment variable (omit it for keyless gateways). The default is `tei`, so
248+
existing Tier 3 configs continue to work unchanged.
249+
243250
For the conceptual background and tuning parameters, see
244251
[Optimize tool discovery](./optimizer.mdx) and
245252
[Tool optimization](../concepts/tool-optimization.mdx).

docs/toolhive/guides-vmcp/optimizer.mdx

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,14 @@ toolset.
8080

8181
## EmbeddingServer resource
8282

83-
The EmbeddingServer CRD manages the lifecycle of a TEI server. An empty
84-
`spec: {}` uses all defaults. The two most important fields you can customize
85-
are:
83+
The EmbeddingServer CRD manages the lifecycle of a managed TEI server, which is
84+
the default embedding backend. If you'd rather point the optimizer at an
85+
external OpenAI-compatible embedding service instead, see
86+
[Use an OpenAI-compatible embedding service](#use-an-openai-compatible-embedding-service)
87+
below.
88+
89+
An empty `spec: {}` uses all defaults. The two most important fields you can
90+
customize are:
8691

8792
- **`model`**: The Hugging Face embedding model to use. The default
8893
(`BAAI/bge-small-en-v1.5`) is the tested and recommended model. You can
@@ -115,6 +120,77 @@ spec:
115120

116121
:::
117122

123+
## Use an OpenAI-compatible embedding service
124+
125+
Instead of running a managed TEI EmbeddingServer, you can point the optimizer at
126+
an external service that speaks the OpenAI `/embeddings` API, such as OpenAI
127+
itself, Azure OpenAI, or another OpenAI-compatible gateway. Use this when you
128+
already operate a centralized embedding service and don't want a second copy
129+
running per vMCP, or when you need a hosted model.
130+
131+
Set `embeddingProvider: openai` under `spec.config.optimizer` and configure
132+
`embeddingService` and `embeddingModel` directly. Do **not** set
133+
`embeddingServerRef`; the operator rejects combining the two at admission.
134+
135+
```yaml title="VirtualMCPServer resource"
136+
apiVersion: toolhive.stacklok.dev/v1beta1
137+
kind: VirtualMCPServer
138+
metadata:
139+
name: optimizer-vmcp
140+
namespace: toolhive-system
141+
spec:
142+
groupRef:
143+
name: my-group
144+
config:
145+
optimizer:
146+
# highlight-start
147+
embeddingProvider: openai
148+
embeddingService: http://llm-gateway.default.svc.cluster.local:8080/v1
149+
embeddingModel: text-embedding-3-small
150+
# highlight-end
151+
embeddingServiceTimeout: 15s
152+
incomingAuth:
153+
type: anonymous
154+
```
155+
156+
`embeddingService` is the base URL of the OpenAI-compatible endpoint;
157+
`/embeddings` is appended automatically. `embeddingModel` is the model name
158+
passed in each request and is required for the `openai` provider (the `tei`
159+
provider ignores it, because the model is fixed by the TEI container).
160+
161+
The API key for the embedding service is read from the `OPENAI_API_KEY`
162+
environment variable on the vmcp container, never from the CRD spec or
163+
ConfigMap. Inject it from a Secret via `podTemplateSpec`:
164+
165+
```yaml title="VirtualMCPServer resource (excerpt)"
166+
spec:
167+
podTemplateSpec:
168+
spec:
169+
containers:
170+
- name: vmcp
171+
env:
172+
- name: OPENAI_API_KEY
173+
valueFrom:
174+
secretKeyRef:
175+
name: embedding-api-key
176+
key: apiKey
177+
```
178+
179+
Omit the env var entirely if your gateway is keyless (for example, an in-cluster
180+
LLM gateway that authenticates by network position). An empty key omits the
181+
`Authorization` header.
182+
183+
:::warning[Inputs are not truncated]
184+
185+
Unlike the TEI backend, the OpenAI API does not silently truncate over-long
186+
inputs. A tool description that exceeds the model's context window causes the
187+
request to fail with an error rather than being truncated.
188+
189+
:::
190+
191+
When `embeddingProvider` is omitted, the optimizer defaults to `tei` and your
192+
existing TEI-based configuration continues to work unchanged.
193+
118194
## Local mode (CLI)
119195

120196
You can enable the optimizer directly from the `thv vmcp` CLI without a
@@ -256,16 +332,23 @@ spec:
256332
exclude={['embeddingService']}
257333
/>
258334

259-
:::info[Kubernetes: EmbeddingServer is always required]
335+
:::info[Kubernetes: EmbeddingServer is required for the default TEI provider]
336+
337+
When using the Kubernetes operator with the default `tei` embedding provider,
338+
even if you set `hybridSearchSemanticRatio` to `"0.0"` (all keyword search), the
339+
optimizer still requires a configured `EmbeddingServer`. The EmbeddingServer
340+
won't be used at runtime when the semantic ratio is `0.0`, but the configuration
341+
must be present due to how the operator wires the resources internally.
260342

261-
When using the Kubernetes operator, even if you set `hybridSearchSemanticRatio`
262-
to `"0.0"` (all keyword search), the optimizer still requires a configured
263-
`EmbeddingServer`. The EmbeddingServer won't be used at runtime when the
264-
semantic ratio is `0.0`, but the configuration must be present due to how the
265-
operator wires the resources internally.
343+
This restriction doesn't apply when you set `optimizer.embeddingService`
344+
directly, such as with the
345+
[OpenAI-compatible provider](#use-an-openai-compatible-embedding-service); the
346+
operator only requires `embeddingServerRef` when no manual embedding service is
347+
configured.
266348

267-
This restriction does not apply to local CLI mode. `thv vmcp serve --optimizer`
268-
runs keyword-only search with no EmbeddingServer and no container.
349+
This restriction also does not apply to local CLI mode.
350+
`thv vmcp serve --optimizer` runs keyword-only search with no EmbeddingServer
351+
and no container.
269352

270353
:::
271354

docs/toolhive/reference/cli/thv_proxy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ thv proxy [flags] SERVER_NAME
9797
### Options
9898

9999
```
100+
--allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com
100101
-h, --help help for proxy
101102
--host string Host for the HTTP proxy to listen on (IP or hostname) (default "127.0.0.1")
102103
--oidc-audience string Expected audience for the token

docs/toolhive/reference/cli/thv_run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...]
112112

113113
```
114114
--allow-docker-gateway Allow outbound connections to Docker gateway addresses (host.docker.internal, gateway.docker.internal, 172.17.0.1). Only applies when --isolate-network is set. These are blocked by default even when insecure_allow_all is enabled.
115+
--allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com
115116
--audit-config string Path to the audit configuration file
116117
--authz-config string Path to the authorization configuration file
117118
--ca-cert string Path to a custom CA certificate file to use for container builds

docs/toolhive/tutorials/mcp-optimizer.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,11 @@ Then apply the YAML above, which creates a new `fetch` server with the correct
181181

182182
## Step 2: Deploy an EmbeddingServer
183183

184-
The optimizer uses semantic search to find relevant tools. This requires an
185-
EmbeddingServer, which runs a text embeddings inference (TEI) server.
184+
The optimizer uses semantic search to find relevant tools, which means it needs
185+
to talk to an embedding service. This tutorial deploys a managed EmbeddingServer
186+
that runs a HuggingFace Text Embeddings Inference (TEI) container. If you'd
187+
rather point at an existing OpenAI-compatible embedding service, see
188+
[Use an OpenAI-compatible embedding service](../guides-vmcp/optimizer.mdx#use-an-openai-compatible-embedding-service).
186189

187190
Create an EmbeddingServer with default settings. This deploys the
188191
`BAAI/bge-small-en-v1.5` model. If you are running on ARM64 nodes (for example,

static/api-specs/toolhive-api.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,12 @@ components:
640640
type: string
641641
type: array
642642
uniqueItems: false
643+
insecure_allow_http:
644+
description: |-
645+
InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.
646+
Only set this for in-cluster Kubernetes deployments on a trusted network.
647+
Production deployments reachable outside the cluster MUST use https://.
648+
type: boolean
643649
issuer:
644650
description: |-
645651
Issuer is the issuer identifier for this authorization server.
@@ -1309,6 +1315,18 @@ components:
13091315
blocked by default in the egress proxy even when InsecureAllowAll is set.
13101316
Only applicable to Docker deployments with network isolation enabled.
13111317
type: boolean
1318+
allowed_origins:
1319+
description: |-
1320+
AllowedOrigins is the allowlist of values accepted on the HTTP Origin header,
1321+
used for DNS-rebinding protection per MCP 2025-11-25 §"Security Warning".
1322+
When empty and Host is loopback (127.0.0.1 / localhost / [::1]), a default
1323+
loopback-only allowlist is derived at middleware-wiring time.
1324+
When empty and Host is non-loopback, the middleware is disabled — operators
1325+
exposing the proxy publicly must configure an explicit allowlist.
1326+
items:
1327+
type: string
1328+
type: array
1329+
uniqueItems: false
13121330
audit_config:
13131331
$ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_audit.Config'
13141332
audit_config_path:

static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"description": "EmbeddedAuthServer configures an embedded OAuth2/OIDC authorization server\nOnly used when Type is \"embeddedAuthServer\"",
129129
"properties": {
130130
"authorizationEndpointBaseUrl": {
131-
"description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.",
131+
"description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts\nwhen insecureAllowHTTP is true) without query, fragment, or trailing slash.",
132132
"pattern": "^https?://[^\\s?#]+[^/\\s?#]$",
133133
"type": "string"
134134
},
@@ -195,8 +195,13 @@
195195
"type": "array",
196196
"x-kubernetes-list-type": "atomic"
197197
},
198+
"insecureAllowHTTP": {
199+
"default": false,
200+
"description": "InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.\nOnly set this for in-cluster Kubernetes deployments where traffic between\npods traverses a trusted network (e.g. the in-cluster service mesh).\nProduction deployments reachable outside the cluster MUST use https://.\n\nOn VirtualMCPServer: when false (the default), http:// issuers for non-localhost\nhosts are rejected at reconcile time with an AuthServerConfigValidated=False condition.\n\nOn MCPServer and MCPRemoteProxy (via MCPExternalAuthConfig): this field is\nstructurally present but enforcement is deferred to pod startup via Config.Validate();\na misconfigured issuer will cause the pod to crash at startup rather than surface\nas an operator condition.",
201+
"type": "boolean"
202+
},
198203
"issuer": {
199-
"description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash (per RFC 8414).",
204+
"description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts when\ninsecureAllowHTTP is true) without query, fragment, or trailing slash (per RFC 8414).",
200205
"pattern": "^https?://[^\\s?#]+[^/\\s?#]$",
201206
"type": "string"
202207
},

0 commit comments

Comments
 (0)