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.54.0
version: v0.55.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
48 changes: 48 additions & 0 deletions docs/toolhive/guides-cli/client-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,54 @@ To remove a client configuration from a specific group:
thv client remove <CLIENT_NAME> --group <GROUP_NAME>
```

## Add remote MCP servers to Claude Desktop

When Claude Desktop routes inference through an LLM gateway (its third-party
mode), it only accepts remote MCP servers from its applied configuration, and
you can't add them from the app. ToolHive calls these entries _connectors_.
Claude Desktop connects to each connector directly, with no local ToolHive
proxy, and runs the server's OAuth sign-in itself.

Connectors are written into the same Claude Desktop configuration that
`thv llm setup` creates, so configure the LLM gateway first:

```bash
thv llm setup --client claude-desktop
```

Then add a connector with a name and the server's URL:

```bash
thv client connector add --client claude-desktop github-tools https://mcp.example.com/mcp
```

The URL must be an absolute `https` URL, and it must be the server's final
address: Claude Desktop doesn't follow HTTP redirects, even one that only adds a
trailing slash. Running `add` again with an existing name updates that
connector's URL.

Claude Desktop reads its configuration only at launch, so fully quit and reopen
it. The new connector shows a **Connect** button under **Customize** >
**Connectors**; click it to sign in to the server.

To list or remove connectors:

```bash
thv client connector list --client claude-desktop
thv client connector rm --client claude-desktop github-tools
```

Re-running `thv llm setup --client claude-desktop` keeps your connectors, and
`thv llm teardown` removes them along with the rest of the gateway
configuration. `thv client connector add` logs a warning when Claude Desktop
won't load the connector, for example when a managed-preferences (MDM) profile
overrides the local configuration or a different saved configuration is applied
in the app.

See the
[`thv client connector` reference](../reference/cli/thv_client_connector.md) for
all options.

## Other clients or tools

If you have other clients or development libraries that ToolHive doesn't
Expand Down
50 changes: 50 additions & 0 deletions docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,56 @@ dcrConfig:
key: token
```

#### Re-register a dropped upstream client

ToolHive caches the client it registers and reuses it across restarts. Each time
the proxy starts, ToolHive checks the cached client at the upstream's token
endpoint. If the upstream answers `invalid_client`, ToolHive registers a new
client and replaces the cached one automatically.

That check can't detect every dropped registration. Some upstreams answer a
request from a dropped public client as if it were still registered, and the
check can't verify `private_key_jwt` clients. In those cases the stale client ID
is only rejected when a user signs in through the browser. To force a fresh
registration, set or change `rotationKey` in `dcrConfig`:

```yaml title="MCPExternalAuthConfig: rotate the DCR client"
dcrConfig:
discoveryUrl: 'https://mcp.example.com/.well-known/oauth-authorization-server'
# highlight-next-line
rotationKey: '2026-10-19'
```

The value is a label of 1 to 64 letters, digits, dots, underscores, or hyphens.
ToolHive uses it only to identify the cached registration and never sends it to
the upstream. When the value changes, the proxy registers a new client at its
next start. Upstream refresh tokens issued to the previous client stop working,
so affected users must sign in again.

### Omit scopes on token refresh

By default, the embedded authorization server sends the configured `scopes` when
it refreshes an upstream token, because some providers narrow the refreshed
token when the `scope` parameter is missing. Providers such as Salesforce reject
refresh requests that include `scope`
(`invalid_request - scope parameter not supported`). For these providers, set
`omitScopeOnRefresh: true` on the upstream's `oidcConfig` or `oauth2Config`:

```yaml title="MCPExternalAuthConfig: omit scope on refresh"
oauth2Config:
authorizationEndpoint: 'https://login.salesforce.com/services/oauth2/authorize'
tokenEndpoint: 'https://login.salesforce.com/services/oauth2/token'
scopes:
- api
- refresh_token
# highlight-next-line
omitScopeOnRefresh: true
```

ToolHive still sends the scopes on the authorization request, so the provider
grants them at sign-in and the refreshed token keeps the originally granted
scopes.

### Extract identity from the token response

Some providers don't expose a userinfo endpoint but return user identity in the
Expand Down
10 changes: 7 additions & 3 deletions docs/toolhive/guides-k8s/run-mcp-k8s.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ When you apply an `MCPServer` resource, here's what happens:
2. The operator automatically creates the necessary RBAC resources in the target
namespace:
- A ServiceAccount with the same name as the MCPServer
- A Role with minimal permissions for StatefulSets, Services, Pods, and Pod
logs/attach operations
- A Role with minimal permissions for StatefulSets, Services, Pods, Pod logs,
and ConfigMaps, plus Pod attach operations for STDIO transport
- A RoleBinding that connects the ServiceAccount to the Role
3. The operator creates a new Deployment containing a ToolHive proxy pod and
service to handle client connections
Expand Down Expand Up @@ -206,7 +206,11 @@ support. Here's what the operator creates automatically:
- StatefulSets (create, get, list, watch, update, patch, delete)
- Services (create, get, list, watch, update, patch, delete)
- Pods (get, list, watch)
- Pod logs and attach operations (get, list)
- Pod logs (get)
- ConfigMaps (get, list, watch)
- Pod attach operations (create, get), granted only when the MCPServer uses
STDIO transport. Servers that use SSE or Streamable HTTP transport reach the
MCP server pod over the network, so their Role omits this permission.
- **RoleBinding**: Connects the ServiceAccount to the Role

This approach provides:
Expand Down
93 changes: 85 additions & 8 deletions docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ When multiple upstream providers are configured, the auth server chains
authorization flows sequentially. The user is redirected to each provider in
order, and the auth server stores each provider's tokens before moving to the
next. After the final provider completes, the auth server issues a single JWT to
the client.
the client. To send each user through only the providers their groups need, see
[Filter upstream providers by group](#filter-upstream-providers-by-group).

## How the references link by name

Expand Down Expand Up @@ -104,8 +105,8 @@ VirtualMCPServer or an MCPServer. The key differences for vMCP are:
VirtualMCPServer resource under `authServerConfig`, rather than in a separate
`MCPExternalAuthConfig` resource.
- **Multiple upstream providers:** vMCP supports multiple upstream providers
with sequential authorization chaining. MCPServer is limited to a single
upstream provider.
with sequential authorization chaining, optionally filtered per user by group.
MCPServer is limited to a single upstream provider.
- **Flexible outgoing strategies:** vMCP uses `upstreamInject` or
`tokenExchange` with `subjectProviderName` to route stored tokens to the
correct backends. MCPServer swaps the token automatically because it has a
Expand Down Expand Up @@ -231,6 +232,76 @@ the deprecated location is planned for the release after the deprecation cycle.

:::

### Filter upstream providers by group

By default, every user signs in to every upstream provider in order. When
different teams use different backends, for example only the sales team uses a
Salesforce backend, add `spec.authServerUpstreamFilter` to choose the providers
each user signs in to based on their groups. Users skip the providers their
groups don't need, and ToolHive collects no tokens for those providers.

The first provider in `upstreamProviders` is mandatory: every user signs in to
it, and ToolHive reads the user's groups from the identity it returns. The
filter then picks which of the remaining providers to add:

```yaml title="VirtualMCPServer: filter upstream providers by group"
spec:
authServerConfig:
issuer: https://auth.example.com
upstreamProviders:
- name: okta # mandatory first provider
type: oidc
# ...
- name: github
type: oauth2
# ...
- name: salesforce
type: oauth2
# ...
# highlight-start
authServerUpstreamFilter:
claim: groups
rules:
- groups: ['engineering']
upstreamProviders: ['github']
- groups: ['sales', 'sales-ops']
upstreamProviders: ['salesforce']
# highlight-end
```

With this configuration, a member of `engineering` signs in to Okta and GitHub,
a member of both `engineering` and `sales` signs in to all three, and a user in
neither group signs in to Okta only.

| Field | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `claim` | Top-level claim that holds the user's groups as a string array. Defaults to `groups`. Nested claims, such as Keycloak's `realm_access.roles`, aren't supported. |
| `rules` | Required, at least one rule. A rule matches when any of the user's groups appears in its `groups` list (exact, case-sensitive). The user signs in to every provider from every matching rule, in configured order. |
| `defaultUpstreams` | Providers to use when the claim is an empty array or no rule matches. When omitted, those users sign in to the first provider only. |

Every name in `rules[].upstreamProviders` and `defaultUpstreams` must be a
configured provider other than the first one. The filter requires
`authServerConfig` with at least two upstream providers; the API server rejects
the resource otherwise. The VirtualMCPServer controller checks the provider
names at reconcile time and sets `AuthServerConfigValidated=False` when one is
unknown or names the first provider.

:::warning[Make sure the first provider emits the groups claim]

ToolHive reads the claim from the first provider's validated ID token (OIDC) or
userinfo response (OAuth 2.0). Groups that appear only in an access token aren't
read. If the claim is missing, authorization fails, so enabling the filter
before the provider emits the claim locks every user out. Configure the provider
to include the claim, and request any scopes it requires, before you add the
filter.

:::

The filter only controls which upstream tokens ToolHive collects at sign-in. A
backend that relies on a skipped provider's token has no token for that user, so
pair the filter with [Cedar policies](./authentication.mdx#authorize-requests)
to control which backends and tools each user can access.

### Handle non-standard provider responses

Not every provider returns tokens and identity in the standard shape. Three
Expand Down Expand Up @@ -471,11 +542,17 @@ registration endpoint bypasses discovery, so keep the authorization endpoint,
token endpoint, and scopes in the parent `oauth2Config`. With `dcrConfig`,
ToolHive obtains the client ID and secret from the DCR response.

| Field | Description |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `dcrConfig.discoveryUrl` | HTTPS URL of an RFC 8414 or OpenID Connect discovery document. |
| `dcrConfig.registrationEndpoint` | HTTPS URL of the RFC 7591 registration endpoint. |
| `dcrConfig.initialAccessTokenRef` | Optional reference to a Secret whose value ToolHive sends as a bearer token when it calls the registration endpoint. Requires `name` and `key`. |
| Field | Description |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dcrConfig.discoveryUrl` | HTTPS URL of an RFC 8414 or OpenID Connect discovery document. |
| `dcrConfig.registrationEndpoint` | HTTPS URL of the RFC 7591 registration endpoint. |
| `dcrConfig.initialAccessTokenRef` | Optional reference to a Secret whose value ToolHive sends as a bearer token when it calls the registration endpoint. Requires `name` and `key`. |
| `dcrConfig.rotationKey` | Optional label (1-64 letters, digits, `.`, `_`, or `-`). Change it to force a fresh registration at the next start; see [Re-register a dropped upstream client](../guides-k8s/embedded-auth-server-k8s.mdx#re-register-a-dropped-upstream-client). |

To stop sending `scope` on refresh token requests to providers that reject it,
such as Salesforce, set `omitScopeOnRefresh: true` on the upstream's
`oidcConfig` or `oauth2Config`. See
[Omit scopes on token refresh](../guides-k8s/embedded-auth-server-k8s.mdx#omit-scopes-on-token-refresh).

### Select the token endpoint auth method

Expand Down
26 changes: 26 additions & 0 deletions docs/toolhive/guides-vmcp/scaling-and-performance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ rebuilds the session from stored metadata and reconnects to backends, so clients
do not need to reinitialize. Without Redis, an evicted session is lost and the
client must reinitialize.

When a client explicitly terminates a session (an HTTP `DELETE` on the session),
the vMCP pod that handles the request removes the session from its cache and
closes its backend connections immediately. Sessions that end without a `DELETE`
stay in the cache until their TTL expires and a later eviction removes them, and
copies cached on other replicas are released the next time those replicas look
up the session.

To serve more than 1,000 concurrent sessions per replica, add vMCP replicas and
configure Redis session storage. Total capacity scales as `replicas × 1,000`.

Expand Down Expand Up @@ -181,6 +188,25 @@ overhead for incoming client connections. The default Linux soft `nofile` limit
is typically 1,024; raise it in the container spec or at the node level if you
expect to serve hundreds of sessions aggregating multiple backends.

### Resource URI and template limits

vMCP bounds the resource URIs and URI templates it matches so that a single
client or backend can't exhaust a pod's CPU or memory:

- **Client resource URIs:** a URI longer than 2,048 bytes in a `resources/read`,
`resources/subscribe`, or `completion/complete` request isn't matched against
resource templates. A URI that exactly equals a backend's concrete resource
URI or template string still resolves at any length.
- **Backend resource templates:** vMCP drops a template that is longer than
1,024 bytes, has more than 8 variables, or whose prefix modifiers (such as
`{id:10}`) total more than 256 characters. The backend's other templates stay
available.
- **Backend resource URIs:** vMCP drops a listed resource whose URI can't be
parsed, and keeps the backend's other resources.

vMCP logs a warning for each URI or template it skips. If a backend's resources
are missing from the aggregated list, check the vMCP pod logs.

### Redis sizing

When you enable Redis session storage, size the Redis instance for the full
Expand Down
1 change: 1 addition & 0 deletions docs/toolhive/reference/cli/thv_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The client command provides subcommands to manage MCP client integrations.
### SEE ALSO

* [thv](thv.md) - ToolHive (thv) is a lightweight, secure, and fast manager for MCP servers
* [thv client connector](thv_client_connector.md) - Manage remote MCP servers configured directly in a client
* [thv client list-registered](thv_client_list-registered.md) - List all registered MCP clients
* [thv client register](thv_client_register.md) - Register a client for MCP server configuration
* [thv client remove](thv_client_remove.md) - Remove a client from MCP server configuration
Expand Down
51 changes: 51 additions & 0 deletions docs/toolhive/reference/cli/thv_client_connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: thv client connector
hide_title: true
description: Reference for ToolHive CLI command `thv client connector`
last_update:
author: autogenerated
slug: thv_client_connector
mdx:
format: md
---

## thv client connector

Manage remote MCP servers configured directly in a client

### Synopsis

Manage remote MCP servers ("connectors") that a client connects to directly,
with no local ToolHive proxy. The client signs in to each server itself (OAuth).

Only Claude Desktop in third-party mode is supported. Run
"thv llm setup --client claude-desktop" first: connectors are written into the
same configuration, and Claude Desktop applies one configuration at a time.

Claude Desktop reads its configuration only at launch, so fully quit and reopen
it after adding or removing a connector. A new connector then shows a Connect
button under Customize → Connectors.

Re-running "thv llm setup --client claude-desktop" keeps existing connectors.
"thv llm teardown" removes them.

### Options

```
--client string Client to configure (supported: claude-desktop)
-h, --help help for connector
```

### Options inherited from parent commands

```
--debug Enable debug mode
```

### SEE ALSO

* [thv client](thv_client.md) - Manage MCP clients
* [thv client connector add](thv_client_connector_add.md) - Add a remote MCP server to a client
* [thv client connector list](thv_client_connector_list.md) - List remote MCP servers configured in a client
* [thv client connector rm](thv_client_connector_rm.md) - Remove a remote MCP server from a client

Loading
Loading