diff --git a/docs/toolhive/guides-cli/skills-management.mdx b/docs/toolhive/guides-cli/skills-management.mdx index 47438467..1d5de960 100644 --- a/docs/toolhive/guides-cli/skills-management.mdx +++ b/docs/toolhive/guides-cli/skills-management.mdx @@ -113,12 +113,31 @@ thv skill install my-skill --force If you have multiple supported clients, ToolHive installs the skill for the first one it detects. To control which client receives the skill, use the -`--client` flag: +`--clients` flag: ```bash -thv skill install my-skill --client claude-code +thv skill install my-skill --clients claude-code ``` +### Install to multiple clients at once + +You can install a skill to multiple clients in a single command by +comma-separating client names: + +```bash +thv skill install my-skill --clients claude-code,cursor +``` + +To install to every skill-supporting client at once, use the special `all` +value: + +```bash +thv skill install my-skill --clients all +``` + +If any client installation fails, ToolHive rolls back all changes for that +install operation. + See the [client compatibility reference](../reference/client-compatibility.mdx) for the full list of clients that support skills. @@ -391,9 +410,11 @@ If your AI client doesn't see an installed skill: thv skill info ``` -3. Verify the skill files exist in the expected directory. For Claude Code, - user-scoped skills are at `~/.claude/skills//` and project-scoped - skills are at `/.claude/skills//`. +3. Verify the skill files exist in the expected directory. For example: + - **Claude Code**: `~/.claude/skills//` (user) or + `/.claude/skills//` (project) + - **Cursor**: `~/.cursor/skills//` (user) or + `/.cursor/skills//` (project) 4. Restart your AI client to trigger skill discovery. diff --git a/docs/toolhive/guides-cli/webhooks.mdx b/docs/toolhive/guides-cli/webhooks.mdx new file mode 100644 index 00000000..64f98ed1 --- /dev/null +++ b/docs/toolhive/guides-cli/webhooks.mdx @@ -0,0 +1,243 @@ +--- +title: Delegate tool authorization with webhooks +sidebar_label: Webhook authorization +description: + Configure webhook middleware to delegate MCP tool call authorization to an + external HTTP service when running MCP servers with the ToolHive CLI. +--- + +Webhook middleware lets you delegate MCP tool call authorization to an external +HTTP service. When a client calls an MCP tool, ToolHive sends a request to your +webhook endpoint, which decides whether to allow or deny the call, and +optionally modify it. + +Use webhooks when your authorization logic is too complex for static Cedar +policies, or when you need to enforce rules managed by an external system (such +as a policy engine or an OPA server). + +## Prerequisites + +- The ToolHive CLI installed. See [Install ToolHive](./install.mdx). +- An HTTP endpoint that accepts webhook requests and returns allow/deny + responses in the ToolHive webhook format. + +## How it works + +When webhook middleware is active, every incoming MCP tool call passes through +two middleware types in order: + +1. **Mutating webhooks** can transform the request before it reaches the MCP + server (for example, to add context or rewrite arguments) +2. **Validating webhooks** accept or deny the (possibly mutated) request + +If a validating webhook denies the request, ToolHive returns an error to the +client without calling the MCP server. + +## Create a webhook configuration file + +Webhook configuration is defined in a YAML or JSON file. The file has two +top-level keys: `validating` and `mutating`. Each key maps to a list of webhook +definitions. + +```yaml title="webhooks.yaml" +validating: + - name: policy-check + url: https://policy.example.com/validate + failure_policy: fail + timeout: 5s + tls_config: + ca_bundle_path: /etc/toolhive/pki/webhook-ca.crt + +mutating: + - name: request-enricher + url: https://enrichment.example.com/mutate + failure_policy: ignore + # Omitting timeout uses the default of 10s. + tls_config: + insecure_skip_verify: true +``` + +### Webhook fields + +| Field | Required | Description | +| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Yes | Unique identifier for this webhook. Used for deduplication when merging multiple config files. | +| `url` | Yes | HTTPS endpoint to call. Plain HTTP is accepted for in-cluster or development use (see `insecure_skip_verify` below). | +| `failure_policy` | Yes | `fail` (deny the request on webhook error) or `ignore` (allow through on error). | +| `timeout` | No | Maximum wait time for a response. Accepts duration strings like `5s` or `30s`. Minimum: `1s`, maximum: `30s`. Default: `10s`. | +| `tls_config` | No | TLS options for the webhook HTTP client (see below). | +| `hmac_secret_ref` | No | Environment variable name containing an HMAC secret for payload signing. **Not yet implemented** - accepted in config but currently has no effect. | + +### TLS configuration + +| Field | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ca_bundle_path` | Path to a CA certificate bundle for server certificate verification. | +| `client_cert_path` | Path to a client certificate for mutual TLS (mTLS). | +| `client_key_path` | Path to the client private key for mTLS. Both `client_cert_path` and `client_key_path` must be set together. | +| `insecure_skip_verify` | Disable TLS certificate verification for HTTPS connections, and also allow plain HTTP endpoint URLs. Use only in development or trusted in-cluster environments. | + +### JSON format + +The same configuration works in JSON format. Timeout values can be either a +duration string (`"5s"`) or a numeric value in nanoseconds: + +```json title="webhooks.json" +{ + "validating": [ + { + "name": "policy-check", + "url": "https://policy.example.com/validate", + "failure_policy": "fail", + "timeout": "5s", + "tls_config": { + "ca_bundle_path": "/etc/toolhive/pki/webhook-ca.crt" + } + } + ], + "mutating": [ + { + "name": "request-enricher", + "url": "https://enrichment.example.com/mutate", + "failure_policy": "ignore", + "tls_config": { + "insecure_skip_verify": true + } + } + ] +} +``` + +## Run an MCP server with webhook middleware + +Pass your webhook configuration file to `thv run` using the `--webhook-config` +flag: + +```bash +thv run fetch --webhook-config /path/to/webhooks.yaml +``` + +You can specify `--webhook-config` multiple times to merge configurations from +several files. If two files define a webhook with the same `name`, the last file +takes precedence: + +```bash +thv run fetch \ + --webhook-config /etc/toolhive/base-webhooks.yaml \ + --webhook-config /etc/toolhive/team-webhooks.yaml +``` + +ToolHive validates all webhook configurations at startup and exits with an error +if any are invalid, so configuration problems surface before the server starts. + +## Failure policies + +The `failure_policy` field controls what happens when ToolHive cannot reach the +webhook endpoint: + +- `fail` denies the MCP tool call. Use this when your webhook is authoritative + and a connectivity failure should be treated as a security event. +- `ignore` allows the tool call through. Use this for non-critical webhooks like + logging or enrichment, where availability is not a hard requirement. + +:::warning + +A `422 Unprocessable Entity` response from a webhook is always treated as a +deny, regardless of the `failure_policy`. This prevents malformed payloads from +accidentally being allowed through. + +::: + +## Webhook request format + +ToolHive sends a JSON `POST` request to your webhook URL with this structure: + +```json +{ + "version": "v0.1.0", + "uid": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-04-13T10:15:30.123Z", + "principal": { + "sub": "user@example.com", + "email": "user@example.com" + }, + "mcp_request": { ... }, + "context": { + "server_name": "fetch", + "source_ip": "127.0.0.1", + "transport": "streamable-http" + } +} +``` + +## Validating webhook response format + +Your validating webhook must respond with HTTP 200 and a JSON body: + +```json +{ + "version": "v0.1.0", + "uid": "550e8400-e29b-41d4-a716-446655440000", + "allowed": true +} +``` + +To deny a request, set `"allowed": false` and optionally include a `message` and +`reason`: + +```json +{ + "version": "v0.1.0", + "uid": "550e8400-e29b-41d4-a716-446655440000", + "allowed": false, + "message": "Tool call denied by policy", + "reason": "insufficient_permissions" +} +``` + +## Mutating webhook response format + +Your mutating webhook must respond with HTTP 200. To pass the request through +unchanged, return an allow response with no patch: + +```json +{ + "version": "v0.1.0", + "uid": "550e8400-e29b-41d4-a716-446655440000", + "allowed": true +} +``` + +To modify the request, include a `patch_type` and a `patch` containing +[JSON Patch](https://jsonpatch.com/) operations. All patch paths must be +prefixed with `/mcp_request/` because the middleware wraps the MCP body in an +envelope before applying patches: + +```json +{ + "version": "v0.1.0", + "uid": "550e8400-e29b-41d4-a716-446655440000", + "allowed": true, + "patch_type": "json_patch", + "patch": [ + { + "op": "add", + "path": "/mcp_request/params/context", + "value": "injected-by-webhook" + } + ] +} +``` + +A mutating webhook can also deny a request by setting `"allowed": false`, in +which case `patch_type` and `patch` are ignored. + +## Next steps + +- [Custom permissions](./custom-permissions.mdx) for Cedar-based authorization + policies for MCP servers +- [Authentication](./auth.mdx) to set up OIDC authentication for MCP servers + +## Related information + +- [`thv run` command reference](../reference/cli/thv_run.md) diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index f62cee60..4e6d6733 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -112,7 +112,7 @@ metadata: namespace: toolhive-system spec: # Remote MCP server URL - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com # Port to expose the proxy on proxyPort: 8080 @@ -203,7 +203,7 @@ providers: ```yaml {4-6} spec: - remoteURL: https://mcp.example.com + remoteUrl: https://mcp.example.com oidcConfig: type: inline @@ -243,7 +243,7 @@ metadata: name: analytics-proxy namespace: toolhive-system spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com oidcConfig: type: configMap configMap: @@ -274,7 +274,7 @@ This example shows different policy patterns: ```yaml {6-9} spec: - remoteURL: https://mcp.example.com + remoteUrl: https://mcp.example.com oidcConfig: # ... OIDC config ... @@ -369,7 +369,7 @@ metadata: name: analytics-proxy namespace: toolhive-system spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com # ... other config ... toolConfigRef: name: analytics-tools-filter @@ -414,7 +414,7 @@ metadata: name: analytics-proxy namespace: toolhive-system spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com oidcConfig: # ... Company IDP config ... @@ -447,7 +447,7 @@ For non-sensitive values like tenant IDs or correlation headers, use ```yaml {6-9} spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com # ... other config ... headerForward: @@ -479,7 +479,7 @@ metadata: name: analytics-proxy namespace: toolhive-system spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com # ... other config ... headerForward: @@ -494,7 +494,7 @@ You can combine plaintext and secret-backed headers: ```yaml {6-14} spec: - remoteURL: https://mcp.analytics.example.com + remoteUrl: https://mcp.analytics.example.com # ... other config ... headerForward: @@ -533,7 +533,7 @@ metadata: name: mcp-spec-proxy namespace: toolhive-system spec: - remoteURL: https://modelcontextprotocol.io/mcp + remoteUrl: https://modelcontextprotocol.io/mcp proxyPort: 8080 transport: streamable-http @@ -688,40 +688,59 @@ Audit logs are structured JSON written to stdout: } ``` -### Prometheus metrics +### Shared telemetry configuration -Enable Prometheus metrics to monitor proxy health and usage: +The preferred way to enable telemetry is to reference a shared +`MCPTelemetryConfig` resource using `telemetryConfigRef`. This lets you define +telemetry settings once and reuse them across multiple proxies and MCP servers: -```yaml {6-8} +```yaml {6-8} title="analytics-proxy.yaml" spec: - remoteURL: https://mcp.example.com + remoteUrl: https://mcp.example.com # ... other config ... - telemetry: - prometheus: - enabled: true + telemetryConfigRef: + name: shared-otel + serviceName: analytics-mcp-proxy ``` -Metrics are exposed at `/metrics` and include: +The `MCPTelemetryConfig` resource is created separately. See +[Telemetry and metrics](./telemetry-and-metrics.mdx) for the full configuration +reference and examples. + +### Available Prometheus metrics + +When Prometheus is enabled, metrics are exposed at `/metrics` and include: - `toolhive_mcp_requests_total` - Total MCP requests - `toolhive_mcp_request_duration_seconds` - Request duration - `toolhive_mcp_tool_calls_total` - Tool call operations - `toolhive_mcp_active_connections` - Number of active connections -### OpenTelemetry +### Inline telemetry configuration (deprecated) + +:::warning[Deprecated] -For distributed tracing and metrics export: +The inline `spec.telemetry` field is deprecated and will be removed in a future +release. Use `telemetryConfigRef` to reference a shared `MCPTelemetryConfig` +resource instead. You cannot set both fields on the same resource. + +::: + +You can still configure telemetry inline using the `telemetry` field: ```yaml {6-15} spec: - remoteURL: https://mcp.example.com + remoteUrl: https://mcp.example.com # ... other config ... telemetry: + prometheus: + enabled: true openTelemetry: enabled: true - endpoint: https://otel-collector.company.com:4317 + endpoint: otel-collector.monitoring.svc.cluster.local:4318 + insecure: true serviceName: analytics-mcp-proxy tracing: enabled: true @@ -765,7 +784,7 @@ metadata: namespace: toolhive-system spec: groupRef: my-group # Reference to an MCPGroup - remoteURL: https://mcp.context7.com/mcp + remoteUrl: https://mcp.context7.com/mcp transport: streamable-http proxyPort: 8080 oidcConfig: diff --git a/docs/toolhive/guides-k8s/telemetry-and-metrics.mdx b/docs/toolhive/guides-k8s/telemetry-and-metrics.mdx index 08306957..527202cf 100644 --- a/docs/toolhive/guides-k8s/telemetry-and-metrics.mdx +++ b/docs/toolhive/guides-k8s/telemetry-and-metrics.mdx @@ -54,9 +54,10 @@ spec: kubectl apply -f shared-otel-config.yaml ``` -**Step 2: Reference from an MCPServer** +**Step 2: Reference from an MCPServer or MCPRemoteProxy** -Reference the config by name in `telemetryConfigRef`: +Reference the config by name in `telemetryConfigRef`. Both `MCPServer` and +`MCPRemoteProxy` support this field: ```yaml {10-12} title="mcpserver-with-shared-otel.yaml" apiVersion: toolhive.stacklok.dev/v1alpha1 @@ -73,6 +74,21 @@ spec: serviceName: mcp-fetch-server ``` +For an `MCPRemoteProxy`, the reference works the same way: + +```yaml {8-10} title="remote-proxy-with-shared-otel.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPRemoteProxy +metadata: + name: analytics-proxy + namespace: toolhive-system +spec: + remoteUrl: https://mcp.analytics.example.com + telemetryConfigRef: + name: shared-otel + serviceName: analytics-mcp-proxy +``` + :::tip[Service name] Set `serviceName` to a meaningful name for each MCP server. This helps identify @@ -184,10 +200,10 @@ mutually exclusive. :::warning[Deprecated] -The inline `spec.telemetry` field on MCPServer is deprecated and will be removed -in a future release. Use `telemetryConfigRef` to reference a shared -MCPTelemetryConfig resource instead. You cannot set both fields on the same -MCPServer. +The inline `spec.telemetry` field on `MCPServer` and `MCPRemoteProxy` is +deprecated and will be removed in a future release. Use `telemetryConfigRef` to +reference a shared `MCPTelemetryConfig` resource instead. You cannot set both +fields on the same resource. ::: diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 1c0853d4..50cb1916 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -189,7 +189,7 @@ spec: source: inline backends: backend-github: - type: external_auth_config_ref + type: externalAuthConfigRef externalAuthConfigRef: name: inject-github ``` @@ -258,11 +258,11 @@ spec: source: inline backends: backend-github: - type: external_auth_config_ref + type: externalAuthConfigRef externalAuthConfigRef: name: inject-github backend-okta-app: - type: external_auth_config_ref + type: externalAuthConfigRef externalAuthConfigRef: name: exchange-okta ``` @@ -577,7 +577,7 @@ spec: source: inline backends: backend-github: - type: external_auth_config_ref + type: externalAuthConfigRef externalAuthConfigRef: name: inject-github ``` diff --git a/docs/toolhive/reference/client-compatibility.mdx b/docs/toolhive/reference/client-compatibility.mdx index 891b30ec..8380eff5 100644 --- a/docs/toolhive/reference/client-compatibility.mdx +++ b/docs/toolhive/reference/client-compatibility.mdx @@ -16,7 +16,7 @@ We've tested ToolHive with these clients: | -------------------------- | :-------: | :----------------: | :------------: | ------------------------------------------- | | GitHub Copilot (VS Code) | ✅ | ✅ | ❌ | v1.102+ or Insiders version ([see note][3]) | | Claude Code | ✅ | ✅ | ✅ | v1.0.27+ | -| Cursor | ✅ | ✅ | ❌ | v0.50.0+ | +| Cursor | ✅ | ✅ | ✅ | v0.50.0+ | | Google Antigravity | ✅ | ✅ | ❌ | | | Gemini CLI | ✅ | ✅ | ❌ | | | Cline (VS Code) | ✅ | ✅ | ❌ | v3.17.10+ | diff --git a/sidebars.ts b/sidebars.ts index e891b856..d7567388 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -108,6 +108,7 @@ const sidebars: SidebarsConfig = { items: [ 'toolhive/guides-cli/auth', 'toolhive/guides-cli/token-exchange', + 'toolhive/guides-cli/webhooks', 'toolhive/guides-cli/telemetry-and-metrics', 'toolhive/guides-cli/test-mcp-servers', 'toolhive/guides-cli/build-containers',