Skip to content

Release v0.51.0 - #6701

Merged
rdimitrov merged 1 commit into
mainfrom
release/v0.51.0
Sep 22, 2026
Merged

rdimitrov merged 1 commit into
mainfrom
release/v0.51.0

Conversation

@toolhive-release-app

Copy link
Copy Markdown
Contributor

Release v0.51.0

Version Bump

minor release

Files Updated

  • VERSION
  • deploy/charts/operator-crds/Chart.yaml (path: version)
  • deploy/charts/operator-crds/Chart.yaml (path: appVersion)
  • deploy/charts/operator/Chart.yaml (path: version)
  • deploy/charts/operator/Chart.yaml (path: appVersion)
  • deploy/charts/operator/values.yaml (path: operator.image)
  • deploy/charts/operator/values.yaml (path: operator.toolhiveRunnerImage)
  • deploy/charts/operator/values.yaml (path: operator.vmcpImage)
  • Helm chart docs (via helm-docs)

Next Steps

  1. Review this PR
  2. Merge to main
  3. Release automation will handle the rest

Checklist

  • Version bump is correct
  • All CI checks pass

Release-Triggered-By: rdimitrov
@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.24%. Comparing base (888fd1f) to head (2be042e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6701      +/-   ##
==========================================
- Coverage   79.30%   79.24%   -0.06%     
==========================================
  Files         799      799              
  Lines       80820    80820              
==========================================
- Hits        64092    64049      -43     
- Misses      16723    16766      +43     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rdimitrov
rdimitrov merged commit 1fa5e36 into main Sep 22, 2026
74 of 75 checks passed
@rdimitrov
rdimitrov deleted the release/v0.51.0 branch September 22, 2026 12:16
@github-actions

Copy link
Copy Markdown
Contributor

📝 Generated release notes for v0.51.0

Auto-generated by the release-notes skill. Review and, if good, apply with:

gh release edit v0.51.0 --notes-file <paste-below>.md
Click to expand release notes

🚀 Toolhive v0.51.0 is live!

A security-hardening release: proxy sessions are now bound to the identity that created them, and two MCP methods that previously skipped the authorizer entirely — completion/complete and subscriptions/listen — are now authorized against the prompt or resource they reference. Operators also get a fix for the MCPServer/MCPGroup CRDs installing with NamesAccepted=False, which had been hanging helm --wait upgrades.

⚠️ Breaking Changes

  • completion/complete and subscriptions/listen are now authorized — both methods used to bypass the authorizer entirely and are now checked against the prompt or resource they reference, so any deployment with authorization enabled must add explicit policy rules (including one naming each URI template string) or these calls will start returning 403. (migration guide)
Migration guide: completion and subscription authorization

Who is affected

Only deployments with authorization enabled. If you have no authz config, the middleware is not installed and nothing changes. Affected surfaces:

  • thv run --authz-config … and any pkg/runner single-server proxy (both the cedarv1 and httpv1 authorizers)
  • Kubernetes MCPServer / operator deployments carrying an authz ConfigMap
  • Virtual MCP servers with Cedar policies (vMCP wires the same middleware)

The most visible symptom is argument autocompletion silently disappearing in IDE and chat clients, because completion is a background UX call. Clients using subscriptions/listen are affected too.

Previously, completion/complete and subscriptions/listen were registered with an empty feature/operation pair, which short-circuited the authorizer — they were allowed unconditionally. They are now resolved from the request body: a ref/prompt requires the same get_prompt decision as prompts/get, and a ref/resource or each subscribed URI requires the same read_resource decision as resources/read.

Before

# Sufficient in v0.50.0 and earlier: completions on ANY prompt or template,
# and subscriptions/listen on ANY URI list, were allowed without a policy check.
permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");
permit(principal, action == Action::"read_resource", resource == Resource::"secrets://tenant/admin");

After

# Prompt completions: this rule is unchanged and now covers completion/complete
# too, because the Cedar entity id is the same prompt name for both methods.
permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");

# Concrete resource read: unchanged.
permit(principal, action == Action::"read_resource", resource == Resource::"secrets://tenant/admin");

# NEW — required for completion/complete with a ref/resource. A completion
# references a URI TEMPLATE, and that string (braces included) is its own Cedar
# entity id. The rule above does not cover it.
permit(principal, action == Action::"read_resource", resource == Resource::"secrets://tenant/{name}");

# NEW — required for subscriptions/listen: every URI in
# notifications.resourceSubscriptions needs its own read_resource permit.
permit(principal, action == Action::"read_resource", resource == Resource::"file:///workspace/config.json");

Migration steps

  1. Inventory your URI templates. Every template the backend returns from resources/templates/list (for example secrets://tenant/{name}) becomes a Cedar Resource:: entity id verbatim, braces included.
  2. Add one read_resource permit per template you want completions on. Name each template explicitly rather than relying on a wildcard — when { resource.uri like "secrets://tenant/*" } matches the template and every concrete URI beneath it, granting more than you probably intend. Granting nothing for a template denies its completions for everyone, which is fail-closed but a change from earlier releases.
  3. Check your prompt rules. If you already grant get_prompt on a prompt name, its completions work with no change. Prompts you deliberately withhold now withhold completions as well — that is the fix.
  4. Add read_resource permits for every URI your clients subscribe to. Subscriptions are all-or-nothing: one denied URI rejects the whole request with 403 rather than narrowing it to the permitted subset.
  5. Audit rules conditioned on arg_*. Derived checks deliberately pass no arguments, so a rule like when { context.arg_env == "dev" } will never match a completion or subscription. Add an argument-free companion rule for these two methods if you need them permitted.
  6. Keep subscriptions/listen lists at or under 50 URIs. The cap counts entries as sent, before de-duplication, so one URI repeated past the limit is refused like any other oversized list. Repeated URIs are evaluated once but still count individually.
  7. Update clients sending the legacy bare-string ref. "ref": "prompt-name" is now rejected because it does not say whether it names a prompt or a resource; send the object form {"type": "ref/prompt", "name": "…"} or {"type": "ref/resource", "uri": "…"}.
  8. Fix clients that serialize unset optional fields as null. "notifications": null and "resourceSubscriptions": null are refused, as is any unknown member of notifications. Omit the member or send [] / {} — absent and empty both pass with zero policy checks.
  9. After upgrading, watch for MCP request denied: no resolvable authorization target at WARN. Note this catches malformed-shape rejections only: a completion denied simply because no permit matches produces no log line, so treat "completions stopped working" as the signal for steps 2–3.

httpv1 (external PDP) users: the same change reaches your PDP as new decisions it never saw before — mcp:prompt:get on mrn:mcp:<server>:prompt:<name>, and mcp:resource:read on mrn:mcp:<server>:resource:<uri-or-template>. Size for the fan-out: one subscriptions/listen can issue up to 50 sequential decisions sharing a single 30-second budget, so a PDP slower than roughly 600 ms per decision will start failing multi-URI subscriptions closed.

Commit: e4e757ba — see docs/authz.md for the full reference

🔒 Security

  • Proxy sessions are bound to their owner — the SSE, Streamable HTTP, and transparent proxies previously resolved a session purely by its ID, so any client that could reach the proxy could present another principal's Mcp-Session-Id or session_id and have its requests routed onto that session, take over the stream, or delete it; sessions are now bound to the validated identity's issuer and subject, and foreign, missing, or unowned sessions get a non-disclosing 404 (upgrade notes). (7937ac76)
  • Completion and subscription requests can no longer reach denied prompts or resources — an identity denied a prompt or resource could previously enumerate its completion candidates, or observe its existence and change timing, through two methods no policy was ever consulted about; see Breaking Changes for the policy rules this now requires. (e4e757ba)
Upgrade notes: session ownership

Most deployments need no action. The binding is on identity, not on the token, so refreshed or rotated tokens for the same (iss, sub) keep working, and deployments with authentication disabled are unaffected — a single local identity binds every session normally.

Sessions that survive the upgrade are rejected once. Records created by the previous version carry no ownership metadata and fail closed with 404 and a JSON-RPC -32001 body. This only applies where session records outlive the proxy process — that is, with Redis session storage configured (typically the operator with horizontal scaling). Default in-memory deployments discard sessions on restart anyway. Per the MCP specification, a client that receives 404 for a request carrying a session ID MUST start a new session with a fresh initialize, so recovery is automatic for conformant clients and needs no operator action.

Two cases do warrant a check before you upgrade:

  1. Confirm your IdP emits iss and sub. A validated identity whose claims lack a non-empty string iss or sub can no longer create or use a session at all, and this case does not self-heal. Standard OIDC and RFC 9068 tokens carry both; custom validators or opaque-token introspection paths that populate claims without iss must be fixed first.
  2. Multi-replica SSE with Redis needs load-balancer session affinity. Redis makes the ownership record visible on every replica; it does not make the stream reachable. A POST landing on a replica that does not hold the client's live SSE socket now returns 503 explicitly instead of being accepted and silently dropped.

Redis outages are distinguished from ownership failures and return 503 rather than 404, so a blip will not tell clients to discard live sessions. Rejecting a request never deletes or closes the owner's session.

Commit: 7937ac76 — see docs/arch/03-transport-architecture.md for the full model

🐛 Bug Fixes

  • The MCPServer and MCPGroup CRDs no longer ship shortNames identical to their own singular and plural, so they stop installing with NamesAccepted=False / ShortNamesConflict and no longer hang helm --wait, Terraform, or Argo CD health checks on the operator-crds chart (#6689, closes #6685).
  • Token exchange now drops JWKS entries it cannot parse instead of failing the whole document, so external issuers that mix understood keys with unknown key types work again — and RSA keys below 2048 bits are rejected on the token-exchange path, matching the OIDC middleware (#6697).
  • The transparent proxy recognizes every session-id spelling servers use in practice (sessionId, sessionid, session_id) across all three sites that read, enforce, and rewrite the carrier, so SSE workloads connect regardless of which name the backend chooses; two spellings carrying different values are refused rather than resolved to one of them (#6698).

🧹 Misc

  • Internal groundwork for SPIFFE client authentication: the embedded authorization server gained JWT-SVID client-assertion validation, but the path is not reachable yet — any non-empty spiffeTrustDomains configuration is still rejected at startup until trust-bundle loading lands (#6563, part of #6199 / #6203).
  • Documented why the winget publisher requires a classic PAT rather than a GitHub App token, so the next release-config cleanup does not "modernize" it and break releases (#6692).
  • Added @Sanskarzz to the documented ToolHive maintainers list (#6700).

📦 Dependencies

Module Version
github.com/aws/aws-sdk-go-v2/config v1.33.5
github.com/aws/aws-sdk-go-v2/service/sts v1.51.0
github.com/ogen-go/ogen v1.24.0
github.com/olekukonko/tablewriter v1.1.5
github.com/onsi/ginkgo/v2 v2.33.0
github.com/onsi/gomega v1.43.1
github.com/sigstore/sigstore v1.11.0
github.com/spiffe/go-spiffe/v2 v2.8.2
github.com/stacklok/toolhive-catalog v0.20260916.0
github.com/stacklok/toolhive-core v0.0.50
github.com/swaggo/swag/v2 v2.0.0-rc6
go.opentelemetry.io/otel/exporters/zipkin v1.45.0
modernc.org/sqlite v1.59.0
sigs.k8s.io/controller-runtime v0.25.1
alpine (image) 3.24.2
ghcr.io/modelcontextprotocol/inspector (image) 2.7.0
anthropics/claude-code-action v1.0.231
codecov/codecov-action v7.1.1
docker/build-push-action v7.4.0
docker/setup-buildx-action v4.4.1
github/codeql-action v4.38.1
ubuntu (CI runner) 26.04

Dependency PRs: #6683, #6693, #6694, #6695, #6696, #6699

👋 Welcome to our newest contributor: @lightsabit 🎉

Full commit log

What's Changed

New Contributors

Full Changelog: v0.50.0...v0.51.0

🔗 Full changelog: v0.50.0...v0.51.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant