Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -44,7 +44,7 @@ projects:

- id: toolhive
repo: stacklok/toolhive
version: v0.42.0
version: v0.42.1
# 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
13 changes: 13 additions & 0 deletions docs/toolhive/concepts/cedar-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,19 @@ permit(principal, action == Action::"call_tool", resource == Tool::"weather") wh
This policy allows weather tool calls only for specific locations, demonstrating
how you can control access based on request parameters.

## JSON request requirement

When Cedar authorization is enabled, every MCP `POST` request must identify its
body as JSON. Use `Content-Type: application/json`; media type matching is
case-insensitive, and parameters such as `charset=utf-8` are accepted. A missing
header, a non-JSON media type, or a near match such as `application/json-rpc`
returns `400 Invalid or malformed MCP request` before ToolHive forwards the
request.

This requirement applies to the transparent proxy and standard MCP endpoints. If
a backend also exposes non-MCP form or upload endpoints, route those endpoints
outside a Cedar-enabled transparent proxy.

## List operations and filtering

List operations (`tools/list`, `prompts/list`, `resources/list`) bypass
Expand Down
28 changes: 24 additions & 4 deletions docs/toolhive/guides-cli/ai-plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ thv ai-plugin install my-plugin

# Project scope
thv ai-plugin install my-plugin --scope project \
--project-root /path/to/project
--project-root .
```

Relative project roots are resolved from the current working directory. You can
also pass an absolute path. The same behavior applies to `install`, `info`,
`list`, and `uninstall`.

### Overwrite or group

Pass `--force` to replace an existing installation of the same plugin, or
Expand All @@ -118,7 +122,7 @@ Filter by client, scope, or group:

```bash
thv ai-plugin list --client claude-code
thv ai-plugin list --scope project --project-root /path/to/project
thv ai-plugin list --scope project --project-root .
thv ai-plugin list --group development
```

Expand All @@ -136,7 +140,7 @@ To see metadata, version, source, and declared contents for an installed plugin:
thv ai-plugin info my-plugin
```

For project-scoped plugins, pass `--scope project --project-root`.
For project-scoped plugins, pass `--scope project --project-root .`.

## Uninstall a plugin

Expand All @@ -148,7 +152,7 @@ For a project-scoped install:

```bash
thv ai-plugin uninstall my-plugin --scope project \
--project-root /path/to/project
--project-root .
```

## Author a plugin
Expand Down Expand Up @@ -271,6 +275,22 @@ Blobs are retained on disk until every tag pointing to their digest is removed.

## Troubleshooting

<details>
<summary>`thv ai-plugin` operation times out</summary>

ToolHive allows API operations up to 10 minutes by default so large OCI pulls
can complete. To use a different client timeout, set `TOOLHIVE_API_TIMEOUT` to a
positive Go duration:

```bash
TOOLHIVE_API_TIMEOUT=30m thv ai-plugin install <PLUGIN_NAME>
```

Invalid, zero, or negative values are ignored. A timeout error means the API
server was reachable but did not respond within the limit.

</details>

<details>
<summary>`thv ai-plugin install` reports "plugin not found in local store or registry"</summary>

Expand Down
47 changes: 43 additions & 4 deletions docs/toolhive/guides-cli/skills-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,12 @@ This installs the skill to your home directory (for example,
### Install a project-scoped skill

```bash
thv skill install my-skill --scope project --project-root /path/to/project
thv skill install my-skill --scope project --project-root .
```

This installs the skill to the project directory (for example,
`/path/to/project/.claude/skills/my-skill/` for Claude Code).
Relative project roots are resolved from the current working directory. You can
also pass an absolute path. This installs the skill inside the selected project
(for example, `<PROJECT_ROOT>/.claude/skills/my-skill/` for Claude Code).

:::note

Expand Down Expand Up @@ -222,6 +223,28 @@ thv skill info <SKILL_NAME>
This shows the skill's name, version, description, scope, status, source
reference, installation date, and associated clients.

## Upgrade skills

Skill upgrades are experimental and require `TOOLHIVE_SKILLS_LOCK_ENABLED=true`
on the ToolHive API server. Upgrade project-scoped skills from the project
directory:

```bash
thv skill upgrade --project-root .
```

A version or tag change within the same OCI repository proceeds without
`--allow-ref-change`, including a move to an older tag. ToolHive still pins the
resolved digest and blocks signer changes. If the catalog moves the skill to a
different repository, organization, or registry, the upgrade is blocked. Review
the new source, then repeat the command with `--allow-ref-change` if you intend
to permit that repository move. ToolHive prompts before installing the planned
changes; pass `--yes` in non-interactive environments.

See the
[`thv skill upgrade` command reference](../reference/cli/thv_skill_upgrade.md)
for all options.

## Uninstall a skill

To remove an installed skill:
Expand All @@ -233,7 +256,7 @@ thv skill uninstall <SKILL_NAME>
For project-scoped skills, specify the scope and project root:

```bash
thv skill uninstall my-skill --scope project --project-root /path/to/project
thv skill uninstall my-skill --scope project --project-root .
```

This removes the skill files from all associated client directories and deletes
Expand Down Expand Up @@ -397,6 +420,22 @@ Then retry your skill command.

</details>

<details>
<summary>Skill installation or upgrade times out</summary>

ToolHive allows API operations up to 10 minutes by default so large OCI pulls
can complete. To use a different client timeout, set `TOOLHIVE_API_TIMEOUT` to a
positive Go duration before running the command:

```bash
TOOLHIVE_API_TIMEOUT=30m thv skill install <SKILL_NAME>
```

Invalid, zero, or negative values are ignored. A timeout error means the API
server was reachable but did not respond within the limit.

</details>

<details>
<summary>Skill not discovered by your AI client</summary>

Expand Down
40 changes: 39 additions & 1 deletion docs/toolhive/guides-k8s/auth-k8s.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,40 @@ kubectl get mcpservers,mcpremoteproxies,virtualmcpservers -n toolhive-system \
| "\(.kind)/\(.metadata.name)"'
```

For an inline config, `issuer` must be a well-formed HTTP(S) URL with a scheme
and host. ToolHive requires HTTPS for both `issuer` and an explicit `jwksUrl`.
For development or testing on a trusted network, set `insecureAllowHTTP: true`
to permit HTTP for both fields. An empty `jwksUrl` remains valid and uses OIDC
discovery.

:::info[Changed in v0.42.1]

The operator validates these URLs whenever it reconciles an inline
`MCPOIDCConfig`, including resources stored before v0.42.1. A malformed URL or
plain HTTP without `insecureAllowHTTP: true` sets the config's `Valid` condition
to `False`. Referencing workloads then set `OIDCConfigRefValidated=False` and
stop reconciling. Existing pods continue running, so check both conditions if a
spec, image, or rollout change does not take effect after an upgrade.

Use HTTPS in production. For development-only HTTP issuers, set the opt-in on
the inline config:

```yaml title="development-oidc-config.yaml"
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: development-oidc
namespace: toolhive-system
spec:
type: inline
inline:
issuer: 'http://keycloak:8080/realms/toolhive'
jwksUrl: 'http://keycloak:8080/realms/toolhive/protocol/openid-connect/certs'
insecureAllowHTTP: true
```

:::

:::note[Upgrading from before v0.21.0?]

Older releases supported an inline `spec.oidcConfig` field directly on
Expand Down Expand Up @@ -399,11 +433,15 @@ kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s

**OIDC configuration issues:**

- For external IdP: Ensure the issuer URL is accessible from within the cluster
- For an external IdP, ensure the issuer and JWKS URLs are valid and accessible
from within the cluster. Both require HTTPS unless the inline config sets
`insecureAllowHTTP: true` for development.
- For Kubernetes auth: Ensure the Kubernetes API server has OIDC enabled
- Check that the JWKS URL returns valid keys
- Verify the MCPOIDCConfig resource is valid:
`kubectl get mcpoidc -n toolhive-system`
- Inspect the validation message:
`kubectl get mcpoidc <NAME> -n toolhive-system -o jsonpath='{.status.conditions[?(@.type=="Valid")]}'`

**Network connectivity:**

Expand Down
62 changes: 52 additions & 10 deletions docs/toolhive/guides-vmcp/composite-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ review independently.

The rest of this guide uses `VirtualMCPCompositeToolDefinition`.

## Describe tool behavior with annotations

Set MCP tool annotations on a composite tool so clients can identify read-only,
destructive, idempotent, or open-world behavior before calling it:

```yaml title="VirtualMCPCompositeToolDefinition (spec)"
spec:
annotations:
title: Gather pull request context
idempotentHint: true
```

The same `annotations` field is available on inline entries under
`spec.config.compositeTools`. Every field is optional:

| Field | Purpose |
| ----------------- | -------------------------------------------------------------- |
| `title` | Human-readable display title for the composite |
| `readOnlyHint` | The composite does not modify its environment |
| `destructiveHint` | The composite may perform destructive updates |
| `idempotentHint` | Repeating the composite with the same input has no new effect |
| `openWorldHint` | The composite interacts with entities outside its local domain |

When you omit annotations, vMCP derives conservative values from the step tools:

- `readOnlyHint` is `true` only when every tool step declares itself read-only.
- `destructiveHint` and `openWorldHint` are `true` when any tool step sets the
hint or leaves it unspecified.
- `idempotentHint` is not derived. Set it explicitly when the complete workflow
is idempotent.

You can set values that are more conservative than the derived values. vMCP
omits a composite from the advertised tool list when `readOnlyHint: true`,
`destructiveHint: false`, or `openWorldHint: false` conflicts with a step tool.
For example, `readOnlyHint: true` conflicts when a step omits `readOnlyHint` or
sets it to `false`. In that case, vMCP logs a warning that identifies the
conflicting steps.

`thv vmcp validate` and Kubernetes reconciliation validate the annotation
structure, but they cannot resolve backend step annotations. Check the running
vMCP logs and `tools/list` response to verify that the composite is advertised.

## Simple example

This composite tool gathers the metadata, diff, and changed file list for a
Expand Down Expand Up @@ -362,7 +404,7 @@ action with a `condition` that checks the `action` output field:
dependsOn: [approval]
defaultResults:
# Required when condition may be false and output block or downstream
# steps reference this step — structure must mirror what downstream expects
# The structure must match what downstream steps expect.
text: '{"status":"skipped"}'
```

Expand Down Expand Up @@ -605,11 +647,11 @@ vMCP validates `defaultResults` at configuration time:
example:

```yaml
# Wrong downstream templates use .output.text, not .output.status
# Wrong: downstream templates use .output.text, not .output.status
defaultResults:
status: 'skipped'

# Correct mirrors the .output.text access pattern
# Correct: mirrors the .output.text access pattern
defaultResults:
text: '{"status":"skipped","id":0}'
```
Expand Down Expand Up @@ -788,13 +830,13 @@ For object properties, use `value` when the step returns data to deserialize, or
```yaml
output:
properties:
# Option 1a: step returns a JSON string already — reference it directly
# Option 1a: the step returns a JSON string, so reference it directly
metadata:
type: object
description: Metadata returned by the backend as a JSON string
value: '{{.steps.fetch.output.metadata_json}}'

# Option 1b: step returns structured data — use json to serialize it first
# Option 1b: the step returns structured data, so serialize it with json
referrers:
type: array
description: OCI referrers attached to this image
Expand Down Expand Up @@ -840,7 +882,7 @@ spec:
description: Repository path without tag, for listing tags
required: [image_ref, repository]
steps:
# All three steps run in parallel no dependsOn between them
# All three steps run in parallel because they have no dependsOn fields
- id: image_info
tool: oci-registry_get_image_info
arguments:
Expand Down Expand Up @@ -872,7 +914,7 @@ spec:
type: integer
description: Number of image layers
value: '{{.steps.image_info.output.layers}}'
# referrers is a structured list — use json to serialize before output parses it
# Serialize the structured referrers list before the output parses it
referrers:
type: array
description:
Expand Down Expand Up @@ -957,7 +999,7 @@ engine renders parsed values using Go's native format, not JSON, so the result
is not valid JSON and type coercion fails.

```yaml
# Wrong Go renders the parsed array as "[map[sha:... filename:...]]"
# Wrong: Go renders the parsed array as "[map[sha:... filename:...]]"
changedFiles:
type: array
description: Changed files
Expand Down Expand Up @@ -1016,7 +1058,7 @@ spec:
- pullNumber
timeout: '5m'
steps:
# All five reads run in parallel no dependsOn between them
# All five reads run in parallel because they have no dependsOn fields
- id: pr_meta
tool: github_pull_request_read
arguments:
Expand Down Expand Up @@ -1105,7 +1147,7 @@ spec:
type: string
description: HTML URL of the pull request
value: '{{(fromJson .steps.pr_meta.output.text).html_url}}'
# GitHub returns JSON text — pass through as string rather than
# GitHub returns JSON text. Pass it through as a string instead of
# using fromJson + type: array (see JSON text in the output block)
changedFiles:
type: string
Expand Down
20 changes: 19 additions & 1 deletion docs/toolhive/guides-vmcp/failure-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ gracefully:
requests to failing backends instead of waiting for timeouts
- **Partial failure modes**: Choose whether to fail entire requests or continue
with available backends
- **Automatic recovery**: Backends are automatically restored when they recover
- **Recovery monitoring**: vMCP keeps probing failed backends so new sessions
can use them after they recover

:::tip

Expand Down Expand Up @@ -175,6 +176,23 @@ GitHub tools are omitted from the response because the circuit breaker is open.
The client doesn't see unavailable backend tools, preventing timeout errors when
attempting to call them.

## Backend selection for new sessions

Before opening a new client session, vMCP checks the health state it already has
for each backend. It skips backends classified as `unhealthy` or
`unauthenticated`, so one known-bad backend does not delay initialization for
the entire vMCP server.

vMCP still attempts backends classified as `degraded` because that state can
also mean a backend has just recovered or is retrying authentication. It also
attempts backends whose state is not known yet, including during startup before
the first health check completes.

This selection applies only when a session opens. A backend skipped at that
point is not attached to the existing session after it recovers. Reconnect the
client to open a new session that includes the recovered backend. Restored
sessions attempt only their original backend set.

## Monitor circuit breaker status

Check backend health and circuit state:
Expand Down
Loading