v0.30.1
📝 Generated release notes for v0.30.1
🚀 Toolhive v0.30.1 is live!
A security-and-resilience focused release: local MCP servers now isolate their network by default, shared MCPAuthzConfig references are enforced end-to-end across every workload kind, OAuth-backed workloads self-recover from transient refresh failures, and OIDC gains per-provider subject-claim selection — alongside the continuing vMCP interface refactor.
⚠️ Breaking & Behavior Changes
- Network isolation is now ON by default for local MCP servers —
thv runand REST-API-created workloads now isolate the container network from the host unless you opt out; servers that reach the host, do proxy-unaware/non-HTTP outbound, or run offline may need action (migration guide) (#5583) - A non-Cedar
authzConfigRefon aVirtualMCPServernow fails reconciliation — the reference is finally enforced at runtime, but because vMCP authz is Cedar-only, a ref to a non-cedarv1MCPAuthzConfig(previously a silent no-op) now setsAuthzConfigRefValidated=Falseand stops updating the workload (migration guide) (#5580)
Migration guide: Network isolation default flip
Who is affected: thv run users and REST API (thv serve) consumers whose servers talk to the host (e.g. host.docker.internal), do non-HTTP or proxy-unaware outbound (notably some Node/npx servers), run under a restrictive permission profile, or run in constrained/offline environments. Already-running servers are unaffected until restarted. Kubernetes/operator behavior is unchanged — the operator does not consume this flag or field.
Before (v0.30.0)
thv run my-server # network isolation OFF (old default)
thv run --isolate-network my-server # explicit opt-inAfter (v0.30.1)
thv run my-server # network isolation ON (new default)
thv run --isolate-network=false my-server # opt out — restores old behavior
thv run --allow-docker-gateway my-server # keep isolation, allow host gateway (host.docker.internal)For the REST API, the network_isolation field changed from bool to an optional *bool — an omitted field now means enabled:
Migration steps
- For any local server that must reach the host or relies on proxy-unaware outbound, add
--isolate-network=false(full opt-out) or--allow-docker-gateway(keep isolation, allow the host gateway). - REST API consumers: regenerate clients from the updated swagger; send
"network_isolation": falseexplicitly for workloads that must not be isolated. - Update/PUT requests built from scratch must include
network_isolationexplicitly — omitting it will enable isolation on a workload that previously had it off (round-trip GET→modify→PUT clients are unaffected, since GET always returns an explicit value).
PR: #5583
Migration guide: VirtualMCPServer non-Cedar authzConfigRef
Who is affected: Operator users with an existing VirtualMCPServer whose spec.incomingAuth.authzConfigRef points at an MCPAuthzConfig of a type other than cedarv1 (e.g. a CEL config). Previously such a reference was a documented no-op and reconciled cleanly while silently enforcing no authorization. It now fails reconciliation. References to a cedarv1 config, inline spec.incomingAuth.authzConfig, or no ref at all are unaffected. The CRD schema is unchanged (only the authzConfigRef field description was updated).
Before
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: my-vmcp
spec:
incomingAuth:
authzConfigRef:
name: my-cel-authz # non-cedarv1 MCPAuthzConfig → previously inert, now rejectedAfter
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPAuthzConfig
metadata:
name: my-cedar-authz
spec:
type: cedarv1
# ... cedar policies ...
---
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: my-vmcp
spec:
incomingAuth:
authzConfigRef:
name: my-cedar-authz # cedarv1 → resolved and enforcedMigration steps
- List affected resources:
kubectl get virtualmcpserver -A -o json | jq -r '.items[] | select(.spec.incomingAuth.authzConfigRef != null) | "\(.metadata.namespace)/\(.metadata.name) -> \(.spec.incomingAuth.authzConfigRef.name)"'. - For each referenced
MCPAuthzConfig, checkspec.type. If it iscedarv1, no action is needed. - If it is anything else, your authorization was never actually enforced. Convert the policy to a
cedarv1MCPAuthzConfigand point the ref at it, or move it inline underspec.incomingAuth.authzConfig(Cedar). These two are mutually exclusive. - To run with no incoming authorization, remove
authzConfigRefentirely (the controller clears the hash and condition). - After upgrade, confirm
AuthzConfigRefValidated=TrueandReady=True. Note: an already-running pod keeps its last-applied policy (fail-stale, not fail-open).
🆕 New Features
- Shared
MCPAuthzConfigreferences (spec.authzConfigRef) are now resolved and enforced at runtime across MCPServer and MCPRemoteProxy (backend-agnostic:cedarv1andhttpv1) and VirtualMCPServer (Cedar-only), instead of being reference-tracked no-ops (#5563, #5564, #5580, foundation #5559) - OIDC upstreams accept an optional
subjectClaim(Go config andsubjectClaimon the CRD) to select which validated ID-token claim is the subject — fixing IdPs wheresubisn't stable, such as Entra/Azure AD's rotatingsub(useoid) (#5589) - The operator can configure a global default Redis/Valkey session store once via
operator.defaultRedis(orglobal.redisin an umbrella chart); workloads without an explicitspec.sessionStoragefall back to it, and per-CR storage always wins (#5595) - The operator now emits Kubernetes
Eventobjects from theMCPOIDCConfig,MCPExternalAuthConfig, andMCPAuthzConfigcontrollers, sokubectl describeshows why a config was rejected or which workloads block its deletion (#5514) - New
WithDialControloption on the vMCPNewHTTPBackendClientlets embedders install a per-connection dialer hook for SSRF / DNS-rebinding defense without abandoning the built-in auth registry and RoundTripper chain (#5551) - New public
NewKubernetesBackendRegistryconstructor gives embedders one-call, live Kubernetes-driven backend discovery without importing the vMCP watch substrate (#5542) - New
PlatformUserIDfield onauth.PrincipalInfoand an exportedNewEmbeddedAuthServerWithStorageconstructor lay the OSS groundwork for per-user-keyed upstream token storage (no runtime change for standalone OSS) (#5575)
🐛 Bug Fixes
- OAuth-backed workloads now self-recover from transient token-refresh failures (e.g. a VPN drop or network-context change) via a new
auth_retryingstatus with tunable cadence and ceiling, instead of going permanently dead (#5350) - The LLM gateway token refresh window is now synced with Claude Code's helper TTL, eliminating the occasional 401 blip at the hourly token boundary (#5557)
- The runner readiness probe now accepts 401/403 from the local proxy when OIDC auth is configured, so auth-protected workloads become ready immediately instead of hanging for the full 5-minute timeout (#5554)
- Operator-managed pods now satisfy the restricted Pod Security Standard on all platforms —
seccompProfile: RuntimeDefaultandcapabilities.drop: [ALL]are set in the base security context, not just on OpenShift (#5555) thv stopandthv rmnow free an orphaned proxy port and stop the proxy even when the workload's status file is missing, instead of silently leaving a port-holding process (#5394)- Allowlisted passthrough headers (e.g.
X-Api-Key) are once again forwarded to backends on the vMCP Serve path, fixing a regression from the legacy path (#5561) - The
MCPOIDCConfigcontroller is now the sole owner of itsReferencingWorkloadsstatus, removing a merge-patch-vs-PUT clobber hazard and an append-only staleness bug (#5544) - OBO
SecretEnvVarsare now wired intoMCPServerandMCPRemoteProxy(previously onlyVirtualMCPServer), so OBO middleware can read its credential at startup in builds with a registered handler (inert in stock builds) (#5540)
🧹 Misc
- Removed the experimental, unauthenticated embedded MCP control-plane server — the
thv mcp servecommand andthv serve --experimental-mcp*flags are gone; use the authenticated REST/OpenAPI API instead (thv mcp call/thv mcp listdebug commands are retained) (#5591) - Routed
vmcpserver construction through the domain core (core.New+Serve) as the single live path; no CLI/operator change, but out-of-tree Go embedders ofpkg/vmcp/servermust now supplyConfig.Aggregatorand useConfig.Authzrather thanConfig.AuthzMiddleware(#5556) - Wired the tool optimizer onto the Serve path so
--enable-optimizerkeeps advertisingfind_tool/call_toolonceserver.Newroutes throughServe(#5543) - Migrated operator test fixtures to the shared
v1beta1testbuilder and added reconciler/scheme test helpers across MCPServer, MCPRemoteProxy, EmbeddingServer, and VirtualMCPServer (#5565, #5566, #5576, #5578, #5579, #5577, #5584) - Added MCPServer and MCPToolConfig integration tests (#5593)
- Isolated
TestWorkloadFromContainerInfostate writes and fixed a data race inTestMCPAuthzConfigReconciler_watchHandlers(#5549, #5558) - Minted the Homebrew tap token from a GitHub App and replaced the retest action with an inline
gh apiscript (#5552, #5548) - Removed broken image links from
SECURITY.mdand fixed a contributing-guide link (#5547, #5587)
📦 Dependencies
| Module | Version |
|---|---|
github.com/stacklok/toolhive-core |
v0.0.25 |
go.mongodb.org/mongo-driver |
v1.17.9 |
github.com/google/go-containerregistry |
v0.21.7 |
github.com/shirou/gopsutil/v4 |
v4.26.5 |
github.com/redis/go-redis/v9 |
v9.20.1 |
github.com/stacklok/toolhive-catalog |
v0.20260623.0 |
golangci/golangci-lint-action |
v9.2.1 |
anthropics/claude-code-action |
2fee155 |
| security scanning & signing actions | v4.1.2 |
| release & publishing actions | (updated) |
👋 Welcome to our newest contributor: @kriptoburak 🎉
Full commit log
What's Changed
- Emit Events from the three config controllers by @ChrisJBurns in #5514
- Wire OBO SecretEnvVars into MCPServer and MCPRemoteProxy by @tgrunnagle in #5540
- Update golangci/golangci-lint-action action to v9.2.1 by @renovate[bot] in #5521
- docs: remove broken image links from SECURITY.md by @dussab in #5547
- Isolate TestWorkloadFromContainerInfo state writes by @ChrisJBurns in #5549
- Mint Homebrew tap token from a GitHub App by @Nashon-Steffen in #5552
- Add WithDialControl option to vMCP backend client by @jhrozek in #5551
- Replace retest action with inline gh api script by @ChrisJBurns in #5548
- Accept 401/403 in runner readiness probe when auth configured by @ChrisJBurns in #5554
- Make Kubernetes security contexts satisfy restricted PSS by @ChrisJBurns in #5555
- Wire the tool optimizer onto the Serve path by @tgrunnagle in #5543
- Recover OAuth workloads from transient refresh failures by @gkatz2 in #5350
- Consolidate MCPOIDCConfig ReferencingWorkloads ownership by @ChrisJBurns in #5544
- Sync LLM token refresh window with helper TTL by @aponcedeleonch in #5557
- Fix data race in TestMCPAuthzConfigReconciler_watchHandlers by @ChrisJBurns in #5558
- Add MCPAuthzConfig ref-resolution foundation by @ChrisJBurns in #5559
- Bump go-containerregistry to v0.21.7 and remove pin by @reyortiz3 in #5562
- Wire MCPAuthzConfig references into the MCPServer controller by @ChrisJBurns in #5563
- Update module github.com/shirou/gopsutil/v4 to v4.26.5 by @renovate[bot] in #5572
- Update anthropics/claude-code-action digest to 2fee155 by @renovate[bot] in #5570
- Update module github.com/redis/go-redis/v9 to v9.20.1 by @renovate[bot] in #5571
- Update security scanning and signing actions to v4.1.2 by @renovate[bot] in #5574
- Wire MCPAuthzConfig references into the MCPRemoteProxy controller by @ChrisJBurns in #5564
- Add testutil.NewScheme helper and migrate operator test schemes by @ChrisJBurns in #5565
- Forward passthrough headers on the Serve path by @tgrunnagle in #5561
- Migrate MCPServer test fixtures to v1beta1test builder by @ChrisJBurns in #5566
- Add v1beta1test builders for vMCP, remote proxy, embedding by @ChrisJBurns in #5576
- Add public Kubernetes-backed dynamic BackendRegistry constructor by @tgrunnagle in #5542
- Wire MCPAuthzConfig references into VirtualMCPServer (Cedar-only) by @ChrisJBurns in #5580
- Fix contributing guide link and bullet by @kriptoburak in #5587
- Migrate MCPRemoteProxy test fixtures to v1beta1test by @ChrisJBurns in #5578
- Migrate EmbeddingServer test fixtures to v1beta1test by @ChrisJBurns in #5579
- Free orphaned proxy port on stop and rm by @gkatz2 in #5394
- Add reconciler test helpers to cut fake-client boilerplate by @ChrisJBurns in #5584
- Update release and publishing actions by @renovate[bot] in #5573
- Default network isolation to on for local MCP servers by @ChrisJBurns in #5583
- Address network isolation review follow-ups by @ChrisJBurns in #5592
- Add MCPServer and MCPToolConfig integration tests by @ChrisJBurns in #5593
- Migrate VirtualMCPServer test fixtures to v1beta1test by @ChrisJBurns in #5577
- Add PlatformUserID and identity-in-context plumbing for token storage by @lorr1 in #5575
- Remove experimental embedded MCP control-plane server by @ChrisJBurns in #5591
- Add global Redis default config support to operator by @reyortiz3 in #5595
- Route server.New through core.New and Serve by @tgrunnagle in #5556
- Bump toolhive-core to v0.0.25 and mongo-driver to v1.17.9 by @reyortiz3 in #5596
- Update module github.com/stacklok/toolhive-catalog to v0.20260623.0 by @renovate[bot] in #5600
- Add per-OIDC-provider SubjectClaim config by @lorr1 in #5589
- Release v0.30.1 by @toolhive-release-app[bot] in #5605
New Contributors
- @kriptoburak made their first contribution in #5587
Full Changelog: v0.30.0...v0.30.1
🔗 Full changelog: v0.30.0...v0.30.1