v0.42.1
🚀 Toolhive v0.42.1 is live!
A security-hardening patch release: three authorization gaps are closed (non-JSON POSTs bypassing Cedar, filtered vMCP tools staying callable, and unvalidated OIDC issuer URLs), alongside a deny-by-default visibility model for vMCP tool aggregation and a fail-closed consent model for external OIDC subject tokens.
⚠️ Breaking Changes
- Non-JSON
POSTrequests are now rejected instead of skipping authorization — with Cedar authorization enabled, aPOSTwithoutContent-Type: application/json(including a missing header) returns400rather than being forwarded unauthorized; set the header on all MCP POSTs (migration guide) - vMCP tools hidden from
tools/listare no longer directly callable — a tool excluded viafilter/excludeAll/excludeAllToolsnow returns-32602on the Modern (2026-07-28) path instead of executing; un-filter it or reach it through a composite tool (migration guide) MCPOIDCConfiginline issuer and JWKS URLs are now validated — stored inline configs with a malformed or plain-HTTP URL flip toValid=Falseon their next reconcile and block reconciliation of every workload referencing them; addinsecureAllowHTTP: trueor switch to HTTPS (migration guide)
Migration guide: non-JSON POSTs are rejected when authorization is enabled
Who is affected: only deployments that configure Cedar authorization (--authz-config, or authzConfig in the CRD). Deployments without an authorization config are entirely unaffected.
Previously, shouldSkipInitialAuthorization skipped Cedar evaluation for any POST whose Content-Type was not application/json — but skipping authorization did not stop the request. The proxy forwarded the body verbatim and MCP backends parse JSON-RPC without checking Content-Type, so a tools/call smuggled under text/plain executed with no policy evaluation at all. Such requests now fall through to the parsed-request check and are refused.
Before
POST /mcp HTTP/1.1
Content-Type: text/plain
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"delete_repo"}}→ forwarded to the backend and executed, with no Cedar evaluation.
After
POST /mcp HTTP/1.1
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"delete_repo"}}→ parsed and evaluated against your Cedar policies. The text/plain form now returns 400 Invalid or malformed MCP request.
Migration steps
- Ensure every MCP client, script, and
curlinvocation sendsContent-Type: application/jsonon POST requests. A missingContent-Typeheader is also now rejected. Spec-conformant MCP Streamable HTTP clients already comply. - Media-type matching is now case-insensitive and parameter-aware, so
Application/JSONandapplication/json; charset=utf-8are accepted. Near-miss types that previously prefix-matched —application/json-rpc,application/jsonx— are not. - If a server behind the transparent proxy also serves non-MCP POST endpoints (form or multipart uploads) and you run Cedar authorization, those requests are now refused too; mount them outside the proxy.
- Alerting keyed on audit outcomes may see new
deniedevents, since these refusals are now audited as denials rather than generic failures.
PR: #6234
Migration guide: hidden vMCP tools are no longer directly callable
Who is affected: vMCP operators using aggregation.tools filter, per-workload excludeAll, or global excludeAllTools, whose clients speak the Modern (2026-07-28) revision.
Tool filtering was enforced on the Legacy path (which registers one handler per advertised tool) but not on the Modern one, which is stateless and resolved tools/call straight against the routing table — and the routing table deliberately holds every backend tool so composite workflow steps can reach them. A Modern client that knew a filtered tool's name could call it successfully. core.CallTool now resolves against the advertised view, so filtering holds identically on both revisions.
Before
aggregation:
tools:
- workload: github
filter: ["get_issue"] # create_issue hidden from tools/listAfter
// Modern client, tools/call { "name": "github_create_issue" }
// → JSON-RPC error -32602, HTTP 400; the backend is never invokedTo keep a tool reachable while hidden from tools/list, wrap it in a composite tool:
compositeTools:
- name: file_issue
steps:
- id: create
type: tool
tool: github.create_issue # workflow steps still reach hidden toolsMigration steps
- If you relied on calling a filtered tool directly by name, remove it from
filter/ dropexcludeAllfor that workload so it appears intools/list— advertised now means callable, and only advertised is callable. - If the tool must stay hidden but reachable, define a composite tool whose step targets it and call the composite by its advertised name. Composite workflow steps are unaffected and still reach hidden backend tools.
- If a client called a tool by its
{workloadID}.{toolName}alias, switch to the exact conflict-resolved name shown intools/list(e.g.github_create_issue). The dotted alias remains valid inside composite workflow step definitions — only directtools/callrejects it. tools/callfor an unknown or hidden tool now answers-32602at HTTP 400 (previously-32603at HTTP 200), matching the MCP specification's "Unknown tool" protocol error. Clients should inspect the JSON-RPC body and treat this as a call-level error, not a connection failure.
Migration guide: MCPOIDCConfig URL validation
Who is affected: clusters with MCPOIDCConfig resources of spec.type: inline whose issuer or jwksUrl is plain HTTP, malformed, missing a scheme or host, or uses a non-HTTP(S) scheme. In practice this is dev/test clusters pointing at an in-cluster Keycloak or Dex over HTTP; production HTTPS setups are unaffected. kubernetesServiceAccount configs are explicitly skipped.
Validation runs at reconcile time, not at admission — so it applies to already-stored objects, not just new applies. A failing config gets Valid=False, and every MCPServer, MCPRemoteProxy, and VirtualMCPServer referencing it gets OIDCConfigRefValidated=False and stops reconciling. Already-running pods keep serving, so a stalled workload can look healthy while silently ignoring spec changes, image updates, and rollouts.
Before
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: keycloak-auth
spec:
type: inline
inline:
issuer: http://keycloak:8080/realms/toolhive
jwksUrl: http://keycloak:8080/realms/toolhive/protocol/openid-connect/certsAfter
# Production — switch to HTTPS
spec:
type: inline
inline:
issuer: https://keycloak.example.com/realms/toolhive
jwksUrl: https://keycloak.example.com/realms/toolhive/protocol/openid-connect/certs
# Dev/test only — opt in explicitly; one flag now covers both URLs
spec:
type: inline
inline:
issuer: http://keycloak:8080/realms/toolhive
jwksUrl: http://keycloak:8080/realms/toolhive/protocol/openid-connect/certs
insecureAllowHTTP: trueMigration steps
- Before upgrading, audit your inline configs:
kubectl get mcpoidcconfigs -A -o jsonpath='{range .items[?(@.spec.type=="inline")]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.inline.issuer}{"\t"}{.spec.inline.jwksUrl}{"\n"}{end}' - Flag any entry whose
issuerorjwksUrlishttp://, has no scheme, or is otherwise malformed. An emptyjwksUrlis fine — it falls back to discovery. - For production, change both URLs to
https://. For dev/test only, addinsecureAllowHTTP: trueunderspec.inline. - After upgrading, verify:
kubectl get mcpoidcconfig <name> -o jsonpath='{.status.conditions[?(@.type=="Valid")]}'. The failure message names the offending URL. - If a workload stalls, check
OIDCConfigRefValidatedon the referencingMCPServer/MCPRemoteProxy/VirtualMCPServer.
🔄 Deprecations
pkg/container/images.NewCompositeKeychaindeprecated in favour ofgithub.com/stacklok/toolhive-core/container/images.NewCompositeKeychain— the local function is now a thin wrapper with identical behaviour and will be removed in a future cleanup wave; Go module consumers only, no CLI or CRD surface (#6147)
🆕 New Features
- vMCP operators can set
aggregation.defaultToolVisibility: denyso that only workloads explicitly listed inaggregation.toolshave their tools advertised, closing the fail-open gap where adding a workload to a group silently exposed it (#6163) - Composite tools now support MCP tool annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint), with a conservative fail-closed safety floor derived from the workflow's step tools when none are set explicitly (#6208) - The embedded auth server accepts
trusted_issuers, letting agents exchange subject tokens minted by an external OIDC issuer (Entra, Okta, Keycloak) for ToolHive-scoped delegated tokens under a fail-closed RFC 8693 consent policy (#6149) TOOLHIVE_API_TIMEOUToverrides the CLI's API client timeout forthv skillandthv ai-plugin, for anyone who wants to fail faster than the new 10-minute default (#6224, #6228)
🐛 Bug Fixes
- vMCP can call tools on dual-era stdio backends again — the removed
logging/setLevelRPC is no longer sent to backends that negotiate MCP 2026-07-28, where the rejection was fatal and closed the session while health checks stayed green (#6184) - A single unhealthy vMCP backend no longer inflates
initializelatency for an entire server: new sessions skip backends the health monitor has classified unhealthy or unauthenticated, while degraded backends are still attempted and restored sessions are unchanged (#6162) - Interactive OIDC login through
thv llm proxynow completes when the calling client times out mid-login — the callback listener is rooted in the proxy's lifetime rather than the inbound request's, which is the normal case forthv llm setup --lazy(#6229) - A slow or failed first JWKS fetch no longer permanently disables token validation for the life of the process;
ErrNotReadyandErrResourceAlreadyExistsare treated as registered, so validation self-heals once a background fetch succeeds (#6221) - JWKS registration passes ToolHive's CA-aware HTTP client per resource, preventing a future
jwxbump from silently bypassing custom CA bundles and private-IP policy on every JWKS fetch (#6220) - Skill and plugin operations that pull OCI artifacts no longer fail on slow or large pulls — the client default rose from 30s to 10 minutes and the skills/plugins routers moved off the flat 60s server cap — and a timeout now says the request timed out instead of claiming the server is unreachable (#6224, #6228)
thv skill upgradeno longer requires--allow-ref-changefor a version change within the same repository; the flag now means "permit the artifact to move to a different repository, org, or registry", and same-repository tag moves — including moves to an older tag — proceed unprompted, with digest pinning and the signer-change guard unchanged (#6225)thv skillcommands accept a relative--project-rootsuch as., resolving it against the working directory instead of failing withproject_root must be absolute(#6223)thv ai-plugincommands accept a relative--project-rootthe same way, matchingthv skill(#6226)
🧹 Misc
- The
defaultToolVisibilityCRD reference no longer carries maintainer-internal defaulting rationale, and an unreachable nil-check was removed from the deny-visibility validator (#6233) pkg/container/imageskeychain logic is delegated totoolhive-corev0.0.37, with the local file reduced to a deprecated wrapper (#6147)
📦 Dependencies
| Module | Version |
|---|---|
github.com/go-git/go-git/v5 |
v5.19.2 (fixes CVE-2026-71556 — worktree operations may follow symlinks) |
📝 Upgrade notes
- Apply the CRDs before the operator.
aggregation.defaultToolVisibilityrequires the v0.42.1 CRDs. Theaggregationsubtree does not preserve unknown fields, so on a cluster running the new operator against old CRDs the field is pruned at admission and aggregation silently falls back toallow— every workload in the group has its tools advertised. Verify withkubectl get virtualmcpserver <name> -o jsonpath='{.spec.config.aggregation.defaultToolVisibility}'. defaultToolVisibilitygates tools only. Resources, resource templates, and prompts from unlisted backends are still advertised.- Composite tools now advertise derived annotations. When
annotationsis not set, a conservative floor is derived from the workflow's step tools; because most backends declare no annotations today, composite tools typically now advertisedestructiveHint: true/openWorldHint: true. These match the MCP specification's defaults for absent annotations, but clients that key off explicit hints may begin prompting for confirmation on composite tools that previously carried none. A contradictory explicit annotation causes the tool to be dropped at advertise time with a warning — this is detected at runtime, not bythv vmcp validateor the operator. - Skipped backends are not re-attached to an existing vMCP session. A backend excluded at session open because it was unhealthy stays absent from that session even after it recovers; reconnect to pick it up.
👋 Welcome to our newest contributors: @lopster568, @SashaMIT 🎉
Full commit log
What's Changed
- Pass CA-aware HTTP client to JWKS registration by @danbarr in #6220
- Handle non-fatal httprc errors in JWKS registration by @danbarr in #6221
- Resolve relative --project-root before the API call by @samuv in #6223
- Stop severing skill artifact pulls at fixed timeouts by @samuv in #6224
- Block only repository moves, not tag moves, on upgrade by @samuv in #6225
- Resolve relative --project-root for ai-plugin too by @samuv in #6226
- Stop severing plugin artifact pulls at fixed timeouts by @samuv in #6228
- Keep the OIDC callback listener alive when an LLM proxy client disconnects by @jhrozek in #6229
- Skip logging/setLevel on a Modern-negotiated session by @amirejaz in #6184
- Add deny-by-default visibility to vMCP aggregation by @jerm-dro in #6163
- Reject tools/call for tools hidden from tools/list by @jerm-dro in #6216
- Support MCP tool annotations for composite tools by @JAORMX in #6208
- Delegate images keychain to toolhive-core by @JAORMX in #6147
- Add a consent model for external OIDC subject tokens by @jhrozek in #6149
- Clean up deny-visibility validator and CRD docs by @jerm-dro in #6233
- Skip known-bad backends when opening vMCP sessions by @jerm-dro in #6162
- Validate MCPOIDCConfig inline issuer and JWKS URLs by @lopster568 in #5936
- Update module github.com/go-git/go-git/v5 to v5.19.2 [SECURITY] by @renovate[bot] in #6244
- fix(authz): reject non-JSON POSTs instead of skipping authorization by @SashaMIT in #6234
- Release v0.42.1 by @toolhive-release-app[bot] in #6247
New Contributors
- @lopster568 made their first contribution in #5936
- @SashaMIT made their first contribution in #6234
Full Changelog: v0.42.0...v0.42.1
🔗 Full changelog: v0.42.0...v0.42.1