Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/upstream-projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ projects:

- id: toolhive
repo: stacklok/toolhive
version: v0.53.0
version: v0.54.0
# toolhive is a monorepo covering the CLI, the Kubernetes
# operator, and the vMCP gateway. It also introduces cross-
# cutting features that land in concepts/, integrations/,
Expand Down
9 changes: 9 additions & 0 deletions docs/toolhive/concepts/embedded-auth-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ the stored upstream access token has expired, ToolHive automatically refreshes
it using the stored refresh token before forwarding the request. Your MCP
session continues without re-authentication.

ToolHive also refreshes when the stored upstream ID token has expired, even if
the access token is still valid. This keeps the ID token current for providers
like Microsoft Entra ID, which issue ID tokens that expire before the access
token, and for features that use the upstream ID token, such as token exchange.
If the provider omits a new ID token from the refresh response, ToolHive keeps
the existing one and doesn't retry until the access token next expires. A failed
ID token refresh doesn't interrupt the session, because the access token is
still valid.

If the refresh token is also expired or has been revoked by the upstream
provider, ToolHive returns a `401` response, prompting re-authentication through
the OAuth flow.
Expand Down
10 changes: 10 additions & 0 deletions docs/toolhive/guides-vmcp/delegate-agent-identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,16 @@ claim, works exactly as in the pre-provisioned case above; the issued token's
`act.sub` is the DCR-assigned `client_id` instead of a statically configured
one.

ToolHive rejects registrations whose keys fall outside these limits:

- RSA keys must have a modulus between 2048 and 8192 bits and an odd public
exponent between 3 and 65537.
- The inline `jwks` can contain at most 10 keys.

ToolHive also checks stored clients against these limits, so a client whose
registered key falls outside them fails authentication with `invalid_client` and
must register again.

## Next steps

- [Accept workload assertions with the JWT-bearer grant](./accept-workload-assertions.mdx)
Expand Down
1 change: 1 addition & 0 deletions docs/toolhive/reference/cli/thv_client_register.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Valid clients:
- lm-studio: LM Studio application
- mistral-vibe: Mistral Vibe IDE
- opencode: OpenCode editor
- pi: Pi coding agent
- qoder: Qoder IDE
- roo-code: VS Code Roo Code extension (deprecated)
- trae: Trae IDE
Expand Down
1 change: 1 addition & 0 deletions docs/toolhive/reference/cli/thv_client_remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Valid clients:
- lm-studio: LM Studio application
- mistral-vibe: Mistral Vibe IDE
- opencode: OpenCode editor
- pi: Pi coding agent
- qoder: Qoder IDE
- roo-code: VS Code Roo Code extension (deprecated)
- trae: Trae IDE
Expand Down
32 changes: 32 additions & 0 deletions docs/toolhive/reference/client-compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ We've tested ToolHive with these clients:
| Mistral Vibe | ✅ | ✅ | ✅ | |
| OpenAI Codex | ✅ | ✅ | ✅ | |
| OpenCode | ✅ | ✅ | ✅ | |
| Pi | ✅ | ✅ | ✅ | v0.99.0+ ([see note][7]) |
| Qoder IDE | ✅ | ✅ | ✅ | |
| Roo Code (VS Code) | ✅ | ✅ | ✅ | Deprecated ([see note][6]) |
| Sourcegraph Amp CLI | ✅ | ✅ | ✅ | |
Expand All @@ -48,6 +49,7 @@ We've tested ToolHive with these clients:
[4]: #stdio-only-client-configuration
[5]: #chatgpt-desktop-configuration
[6]: #roo-code-and-cline
[7]: #pi

The minimum versions listed are the earliest versions that support the
Streamable HTTP transport protocol.
Expand Down Expand Up @@ -316,6 +318,36 @@ Example configuration:
When you register the Copilot CLI as a client, ToolHive automatically updates
this file whenever you run an MCP server.

### Pi

The [Pi coding agent](https://pi.dev/) stores its global MCP configuration in a
JSON file in your home directory.

- **All platforms**: `~/.pi/agent/mcp.json`

Example configuration:

```json
{
"mcpServers": {
"github": { "url": "http://localhost:19046/mcp", "type": "http" },
"fetch": { "url": "http://localhost:43832/mcp", "type": "http" }
}
}
```

Pi supports the Streamable HTTP transport only. ToolHive adds SSE servers to the
file without a `type` field, but Pi treats them as Streamable HTTP and can't
connect to them.

If you've installed a Pi extension that provides its own MCP support, such as
`pi-mcp-adapter`, Pi ignores `mcp.json`, including the servers ToolHive
configures. Remove the extension to use Pi's built-in MCP support.

When you register Pi as a client, ToolHive automatically updates this file
whenever you run an MCP server. ToolHive installs skills to `~/.pi/agent/skills`
for user scope and `.pi/skills` for project scope.

## Manual configuration

If your client doesn't support automatic configuration, you'll need to set up
Expand Down
6 changes: 3 additions & 3 deletions docs/toolhive/reference/helm/operator-crds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Control CRD installation and preservation with these Helm values.
This reference lists values declared in the chart's default `values.yaml`.
Templates can also accept optional settings and inherited `global` values.

Chart: `toolhive-operator-crds`. Chart version: `0.53.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.53.0/deploy/charts/operator-crds/values.yaml)
at `v0.53.0`.
Chart: `toolhive-operator-crds`. Chart version: `0.54.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.54.0/deploy/charts/operator-crds/values.yaml)
at `v0.54.0`.

## Values

Expand Down
18 changes: 9 additions & 9 deletions docs/toolhive/reference/helm/operator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Configure the ToolHive operator with these Helm values.
This reference lists values declared in the chart's default `values.yaml`.
Templates can also accept optional settings and inherited `global` values.

Chart: `toolhive-operator`. Chart version: `0.53.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.53.0/deploy/charts/operator/values.yaml)
at `v0.53.0`.
Chart: `toolhive-operator`. Chart version: `0.54.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.54.0/deploy/charts/operator/values.yaml)
at `v0.54.0`.

## Values

Expand Down Expand Up @@ -68,7 +68,7 @@ features:
gc:
gogc: 75
gomemlimit: 110MiB
image: ghcr.io/stacklok/toolhive/operator:v0.53.0
image: ghcr.io/stacklok/toolhive/operator:v0.54.0
imageDiscovery:
enabled: false
resources: {}
Expand Down Expand Up @@ -153,8 +153,8 @@ serviceAccount:
labels: {}
name: toolhive-operator
tolerations: []
toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.53.0
vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.53.0
toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.54.0
vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.54.0
volumeMounts: []
volumes: []
```
Expand Down Expand Up @@ -255,7 +255,7 @@ Key containing the password in `existingSecret`. When empty, uses

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.53.0"`
**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.54.0"`

Container image for the operator.

Expand All @@ -271,15 +271,15 @@ Image pull policy for the operator container and image-discovery containers.

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.53.0"`
**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.54.0"`

Container image for ToolHive proxy runners created by the operator.

### `operator.vmcpImage`

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.53.0"`
**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.54.0"`

Container image for Virtual MCP Server (vMCP) deployments created by the
operator.
Expand Down
19 changes: 14 additions & 5 deletions static/api-specs/toolhive-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -764,20 +764,22 @@ components:
Profile selects how the endpoint's TLS connection is authenticated:
SPIFFEBundleEndpointProfileHTTPSWeb (Web PKI) or
SPIFFEBundleEndpointProfileHTTPSSPIFFE (a separately distributed
X.509-SVID root). Required, since the future bundle loader cannot
otherwise know which trust anchor to use for the initial connection.
X.509-SVID root). Required, since an endpoint loader needs this profile
to choose the trust anchor for its initial connection.
type: string
url:
type: string
type: object
authserver.SPIFFEBundleSourceRunConfig:
description: |-
BundleSource declares exactly one future trust-bundle source. It is
validated for shape only; fetching or loading a bundle from it is a
later step.
BundleSource declares exactly one trust-bundle source and is validated for
shape here. The standalone file loader exists, but source initialization
is not wired into SPIFFE authentication.
properties:
endpoint:
$ref: '#/components/schemas/authserver.SPIFFEBundleEndpointSourceRunConfig'
file:
$ref: '#/components/schemas/authserver.SPIFFEFileBundleSourceRunConfig'
type:
type: string
workload_api:
Expand Down Expand Up @@ -844,6 +846,11 @@ components:
this association policy.
type: string
type: object
authserver.SPIFFEFileBundleSourceRunConfig:
properties:
path:
type: string
type: object
authserver.SPIFFETrustDomainRunConfig:
properties:
bundle_source:
Expand Down Expand Up @@ -1268,6 +1275,7 @@ components:
- factory
- copilot-cli
- qoder
- pi
type: string
x-enum-varnames:
- RooCode
Expand Down Expand Up @@ -1295,6 +1303,7 @@ components:
- Factory
- CopilotCli
- Qoder
- Pi
github_com_stacklok_toolhive_pkg_client.ClientAppStatus:
properties:
client_type:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@
"description": "SPIFFETrustDomainConfig declares one SPIFFE trust domain accepted by the\nembedded authorization server. Configuration is not authentication: no\nlive X.509-SVID or JWT-SVID validation exists yet, so a declared trust\ndomain does not by itself let any workload authenticate — RunConfig.Validate\n(pkg/authserver/config.go) currently hard-rejects any non-empty\nspiffeTrustDomains at authserver startup via validateSPIFFENotYetEnforced,\na deliberate placeholder until real SVID verification lands.",
"properties": {
"bundleSource": {
"description": "BundleSource declares exactly one future trust-bundle source. It is\nvalidated for shape only; fetching or loading a bundle from it is a\nlater step.",
"description": "BundleSource declares exactly one trust-bundle source and is validated for\nshape here. The standalone file loader exists, but is not wired into SPIFFE\nauthentication.",
"properties": {
"endpoint": {
"description": "Endpoint declares a HTTPS SPIFFE Bundle Endpoint. Required when Type is\n\"bundle_endpoint\".",
Expand All @@ -712,10 +712,40 @@
],
"type": "object"
},
"file": {
"description": "File declares a SPIFFE trust bundle projected from a ConfigMap.\nRequired when Type is \"file\".",
"properties": {
"configMapKey": {
"description": "ConfigMapKey is the key within the ConfigMap holding the SPIFFE JWKS\ntrust-bundle document. It must not be \".\" or start with \"..\".",
"maxLength": 253,
"minLength": 1,
"pattern": "^[A-Za-z0-9._-]+$",
"type": "string",
"x-kubernetes-validations": [
{
"rule": "self != '.' && !self.startsWith('..')"
}
]
},
"configMapName": {
"description": "ConfigMapName is the name of the ConfigMap containing the trust bundle.",
"maxLength": 253,
"minLength": 1,
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$",
"type": "string"
}
},
"required": [
"configMapKey",
"configMapName"
],
"type": "object"
},
"type": {
"description": "Type selects the trust-bundle source.",
"enum": [
"bundle_endpoint",
"file",
"workload_api"
],
"type": "string"
Expand All @@ -734,6 +764,10 @@
"message": "endpoint configuration must be set if and only if type is 'bundle_endpoint'",
"rule": "self.type == 'bundle_endpoint' ? has(self.endpoint) : !has(self.endpoint)"
},
{
"message": "file configuration must be set if and only if type is 'file'",
"rule": "self.type == 'file' ? has(self.file) : !has(self.file)"
},
{
"message": "workloadAPI configuration must be set if and only if type is 'workload_api'",
"rule": "self.type == 'workload_api' ? has(self.workloadAPI) : !has(self.workloadAPI)"
Expand Down
36 changes: 35 additions & 1 deletion static/api-specs/toolhive-crds/virtualmcpservers.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@
"description": "SPIFFETrustDomainConfig declares one SPIFFE trust domain accepted by the\nembedded authorization server. Configuration is not authentication: no\nlive X.509-SVID or JWT-SVID validation exists yet, so a declared trust\ndomain does not by itself let any workload authenticate — RunConfig.Validate\n(pkg/authserver/config.go) currently hard-rejects any non-empty\nspiffeTrustDomains at authserver startup via validateSPIFFENotYetEnforced,\na deliberate placeholder until real SVID verification lands.",
"properties": {
"bundleSource": {
"description": "BundleSource declares exactly one future trust-bundle source. It is\nvalidated for shape only; fetching or loading a bundle from it is a\nlater step.",
"description": "BundleSource declares exactly one trust-bundle source and is validated for\nshape here. The standalone file loader exists, but is not wired into SPIFFE\nauthentication.",
"properties": {
"endpoint": {
"description": "Endpoint declares a HTTPS SPIFFE Bundle Endpoint. Required when Type is\n\"bundle_endpoint\".",
Expand All @@ -603,10 +603,40 @@
],
"type": "object"
},
"file": {
"description": "File declares a SPIFFE trust bundle projected from a ConfigMap.\nRequired when Type is \"file\".",
"properties": {
"configMapKey": {
"description": "ConfigMapKey is the key within the ConfigMap holding the SPIFFE JWKS\ntrust-bundle document. It must not be \".\" or start with \"..\".",
"maxLength": 253,
"minLength": 1,
"pattern": "^[A-Za-z0-9._-]+$",
"type": "string",
"x-kubernetes-validations": [
{
"rule": "self != '.' && !self.startsWith('..')"
}
]
},
"configMapName": {
"description": "ConfigMapName is the name of the ConfigMap containing the trust bundle.",
"maxLength": 253,
"minLength": 1,
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$",
"type": "string"
}
},
"required": [
"configMapKey",
"configMapName"
],
"type": "object"
},
"type": {
"description": "Type selects the trust-bundle source.",
"enum": [
"bundle_endpoint",
"file",
"workload_api"
],
"type": "string"
Expand All @@ -625,6 +655,10 @@
"message": "endpoint configuration must be set if and only if type is 'bundle_endpoint'",
"rule": "self.type == 'bundle_endpoint' ? has(self.endpoint) : !has(self.endpoint)"
},
{
"message": "file configuration must be set if and only if type is 'file'",
"rule": "self.type == 'file' ? has(self.file) : !has(self.file)"
},
{
"message": "workloadAPI configuration must be set if and only if type is 'workload_api'",
"rule": "self.type == 'workload_api' ? has(self.workloadAPI) : !has(self.workloadAPI)"
Expand Down
Loading