diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index d58e1c39..2e494648 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -35,7 +35,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.26.1 + version: v0.27.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/, diff --git a/docs/toolhive/guides-cli/api-server.mdx b/docs/toolhive/guides-cli/api-server.mdx index 02db3cbf..1489779e 100644 --- a/docs/toolhive/guides-cli/api-server.mdx +++ b/docs/toolhive/guides-cli/api-server.mdx @@ -57,17 +57,29 @@ address to bind to using the `--host` option: thv serve --host ``` -## UNIX socket support +## UNIX socket and Windows named-pipe support -The API server can also be exposed via a UNIX socket instead of a TCP port. Use -the `--socket` option to specify a socket path: +The API server can also be exposed via a UNIX socket or Windows named pipe +instead of a TCP port. Use the `--socket` option to specify the address. + +On macOS and Linux, pass a filesystem path for a UNIX domain socket: ```bash thv serve --socket /tmp/toolhive.sock ``` -When using a UNIX socket, the `--socket` argument overrides the host:port -address configuration. +On Windows, pass a named-pipe path: + +```powershell +thv serve --socket \\.\pipe\thv-api +``` + +ToolHive auto-detects the address type from its prefix: addresses starting with +`\\.\pipe\` open as a Windows named pipe; anything else opens as a UNIX domain +socket. Named-pipe addresses are rejected on non-Windows platforms. + +When using `--socket`, the argument overrides the host:port address +configuration. ## API documentation diff --git a/docs/toolhive/guides-cli/run-mcp-servers.mdx b/docs/toolhive/guides-cli/run-mcp-servers.mdx index 33e85dc7..0314ea63 100644 --- a/docs/toolhive/guides-cli/run-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/run-mcp-servers.mdx @@ -684,9 +684,21 @@ thv run https://protected-api.com/mcp --name my-server ``` If authentication is required, ToolHive will prompt you to complete the OAuth -flow. When no client credentials are provided, ToolHive automatically registers -an OAuth client with the authorization server using RFC 7591 dynamic client -registration, eliminating the need to pre-configure client ID and secret. +flow. When no client credentials are provided, ToolHive identifies itself to the +authorization server using one of two mechanisms: + +- **Client ID Metadata Document (CIMD).** If the authorization server's + discovery document sets `client_id_metadata_document_supported: true`, + ToolHive presents + [`https://toolhive.dev/oauth/client-metadata.json`](https://toolhive.dev/oauth/client-metadata.json) + as its `client_id`. No registration round-trip is needed. +- **Dynamic Client Registration (DCR, RFC 7591).** When CIMD is unavailable or + the authorization server rejects the CIMD `client_id` with + `invalid_client`/`unauthorized_client`, ToolHive falls back to registering an + OAuth client dynamically. + +Either path eliminates the need to pre-configure a client ID and secret for +authorization servers that support them. #### OIDC authentication @@ -760,12 +772,9 @@ as defined by [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707). This allows the authorization server to return an access token with a scoped audience, which will then be passed to and validated by the remote MCP server. -By default, ToolHive automatically uses the remote server URL as the resource -indicator when authenticating. The URL is validated, normalized (lowercase -scheme and host, fragments stripped), and included in the OAuth token request. - -To explicitly set a different resource indicator, use the -`--remote-auth-resource` flag: +ToolHive does not auto-derive the resource indicator from the server URL by +default. Set it explicitly with `--remote-auth-resource` when the authorization +server requires it: ```bash thv run https://api.example.com/mcp \ diff --git a/docs/toolhive/guides-cli/webhooks.mdx b/docs/toolhive/guides-cli/webhooks.mdx index 64f98ed1..fc1d0d84 100644 --- a/docs/toolhive/guides-cli/webhooks.mdx +++ b/docs/toolhive/guides-cli/webhooks.mdx @@ -15,6 +15,16 @@ 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). +:::note[Kubernetes equivalent] + +In a Kubernetes deployment, configure webhook middleware declaratively with the +[`MCPWebhookConfig`](../reference/crds/mcpwebhookconfig.mdx) custom resource and +reference it from an `MCPServer` via `spec.webhookConfigRef`. The runtime +semantics described below (request and response formats, failure policies, +mutating vs. validating) are identical. + +::: + ## Prerequisites - The ToolHive CLI installed. See [Install ToolHive](./install.mdx). @@ -241,3 +251,5 @@ which case `patch_type` and `patch` are ignored. ## Related information - [`thv run` command reference](../reference/cli/thv_run.md) +- [`MCPWebhookConfig` CRD reference](../reference/crds/mcpwebhookconfig.mdx) - + configure webhook middleware declaratively in Kubernetes diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index fab1673a..c841844e 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -648,10 +648,19 @@ For a complete walkthrough, see By default, the embedded authorization server stores sessions in memory. Upstream tokens are lost when pods restart, requiring users to re-authenticate. -For production deployments, configure Redis Sentinel as the storage backend by -adding a `storage` block to your `MCPExternalAuthConfig`: - -```yaml title="storage block for MCPExternalAuthConfig" +For production deployments, configure a Redis backend by adding a `storage` +block to your `MCPExternalAuthConfig`. The `redis` block supports three +connection modes; you must set exactly one: + +- **Sentinel** (`sentinelConfig`) - self-managed Redis with Sentinel-based high + availability (HA) +- **Standalone** (`addr` only) - managed Redis services that expose a single + endpoint, such as GCP Memorystore Basic/Standard or Azure Cache for Redis +- **Cluster** (`addr` with `clusterMode: true`) - managed Redis Cluster + services, such as GCP Memorystore Cluster or AWS ElastiCache with cluster mode + enabled + +```yaml title="storage block for MCPExternalAuthConfig - Sentinel" storage: type: redis redis: @@ -669,17 +678,44 @@ storage: key: password ``` -Create the Secret containing your Redis ACL credentials: +```yaml title="storage block for MCPExternalAuthConfig - Standalone" +storage: + type: redis + redis: + addr: redis.example.com:6379 + aclUserConfig: + # usernameSecretRef is optional - omit for managed tiers without ACL + # users (GCP Memorystore Basic/Standard, Azure Cache for Redis) + passwordSecretRef: + name: redis-acl-secret + key: password +``` + +```yaml title="storage block for MCPExternalAuthConfig - Cluster" +storage: + type: redis + redis: + addr: redis-cluster.example.com:6379 + clusterMode: true + aclUserConfig: + passwordSecretRef: + name: redis-acl-secret + key: password +``` + +Create the Secret containing your Redis credentials. The example below includes +the username for ACL-enabled deployments; omit `--from-literal=username=...` +when targeting a managed tier that does not support ACL users: ```bash kubectl create secret generic redis-acl-secret \ --namespace toolhive-system \ --from-literal=username=toolhive-auth \ - --from-literal=password="YOUR_REDIS_ACL_PASSWORD" + --from-literal=password="YOUR_REDIS_PASSWORD" ``` For a complete walkthrough including deploying Redis Sentinel from scratch, see -[Redis Sentinel session storage](./redis-session-storage.mdx). +[Redis session storage](./redis-session-storage.mdx). ### Using an OAuth 2.0 upstream provider diff --git a/docs/toolhive/guides-k8s/redis-session-storage.mdx b/docs/toolhive/guides-k8s/redis-session-storage.mdx index fb3b3e62..390d815f 100644 --- a/docs/toolhive/guides-k8s/redis-session-storage.mdx +++ b/docs/toolhive/guides-k8s/redis-session-storage.mdx @@ -1,16 +1,18 @@ --- title: Redis session storage description: - Deploy Redis Sentinel for the ToolHive embedded auth server, or a standalone - Redis instance for MCPServer and VirtualMCPServer horizontal scaling. + Deploy Redis Sentinel, standalone Redis, or Redis Cluster as the session store + for the ToolHive embedded auth server and horizontal scaling. --- ToolHive uses Redis for several purposes. This page covers two that require different configuration: - **Embedded authorization server sessions** - stores upstream tokens so users - don't need to re-authenticate after pod restarts. Uses Redis Sentinel with - ACL-based authentication and a fixed `thv:auth:*` key pattern. See + don't need to re-authenticate after pod restarts. Supports Sentinel, + standalone, and Redis Cluster connection modes. Uses ACL-based authentication + (optional for managed tiers without ACL users) and a fixed `thv:auth:*` key + pattern. See [Embedded auth server session storage](#embedded-auth-server-session-storage). - **MCPServer and VirtualMCPServer horizontal scaling** - shares MCP session @@ -31,12 +33,31 @@ You can reuse the same Redis instance for all three purposes by using different ## Embedded auth server session storage -Configure Redis Sentinel as the session storage backend for the ToolHive +Configure Redis as the session storage backend for the ToolHive [embedded authorization server](../concepts/embedded-auth-server.mdx). By default, sessions are stored in memory, which means upstream tokens are lost -when pods restart and users must re-authenticate. Redis Sentinel provides -persistent storage with automatic master discovery, ACL-based access control, -and optional failover when replicas are configured. +when pods restart and users must re-authenticate. Redis provides persistent +storage that survives pod restarts. + +The `redis` block on `MCPExternalAuthConfig` supports three connection modes, +and exactly one must be set: + +- **Sentinel** (`sentinelConfig`) - self-managed Redis with Sentinel-based high + availability (HA), automatic master discovery, and optional failover when + replicas are configured. Covered in the walkthrough below. +- **Standalone** (`addr` only) - a single endpoint, such as a managed Redis + service that handles HA internally (GCP Memorystore Basic/Standard, Azure + Cache for Redis, AWS ElastiCache without cluster mode). See + [Managed Redis: standalone and cluster modes](#managed-redis-standalone-and-cluster-modes). +- **Cluster** (`addr` with `clusterMode: true`) - a single discovery endpoint + for a Redis Cluster service that auto-discovers the full shard topology (GCP + Memorystore Cluster, AWS ElastiCache with cluster mode). See + [Managed Redis: standalone and cluster modes](#managed-redis-standalone-and-cluster-modes). + +The rest of this section walks through deploying Redis Sentinel and wiring it +into `MCPExternalAuthConfig`. For managed Redis services, follow the same TLS, +ACL, and verification steps but use the standalone or Cluster `storage` block +instead of `sentinelConfig`. :::info[Prerequisites] @@ -418,6 +439,64 @@ storage: For the complete list of storage configuration fields, see the [Kubernetes CRD reference](../reference/crds/mcpexternalauthconfig.mdx#specembeddedauthserverstorage). +### Managed Redis: standalone and cluster modes + +Managed Redis services typically expose a single endpoint and handle HA +internally, so the Sentinel walkthrough above does not apply to them. Use one of +the connection modes below instead. + +#### Standalone mode + +Use `addr` on its own for managed services that present a single, non-clustered +endpoint, such as GCP Memorystore Basic or Standard HA, Azure Cache for Redis, +or AWS ElastiCache without cluster mode enabled: + +```yaml title="storage block with standalone addr" +storage: + type: redis + redis: + addr: redis.example.com:6379 + aclUserConfig: + # usernameSecretRef is optional - omit on managed tiers that do not + # expose ACL users (GCP Memorystore Basic/Standard HA, Azure Cache + # for Redis). Connections fall back to legacy password-only AUTH. + passwordSecretRef: + name: redis-acl-secret + key: password +``` + +When `usernameSecretRef` is omitted, ToolHive authenticates with password-only +`AUTH`, which works against managed tiers that lack Redis ACL support. When the +managed service supports ACL users (for example, AWS ElastiCache non-cluster +with Redis 6+ RBAC), set `usernameSecretRef` to use that user. + +#### Cluster mode + +Set `clusterMode: true` alongside `addr` when the address points at a Redis +Cluster discovery endpoint, such as GCP Memorystore Cluster or AWS ElastiCache +with cluster mode enabled. ToolHive auto-discovers the full shard topology from +the single endpoint: + +```yaml title="storage block with Redis Cluster" +storage: + type: redis + redis: + addr: redis-cluster.example.com:6379 + clusterMode: true + aclUserConfig: + passwordSecretRef: + name: redis-acl-secret + key: password +``` + +:::note + +`addr` and `sentinelConfig` are mutually exclusive - the CRD admission rules +reject manifests that set both or neither. `clusterMode: true` requires `addr` +and cannot be combined with `sentinelConfig`. + +::: + ## Enable TLS Without TLS, Redis credentials and session tokens travel in plaintext between diff --git a/docs/toolhive/guides-vmcp/backend-discovery.mdx b/docs/toolhive/guides-vmcp/backend-discovery.mdx index 64bbdb09..85f7d997 100644 --- a/docs/toolhive/guides-vmcp/backend-discovery.mdx +++ b/docs/toolhive/guides-vmcp/backend-discovery.mdx @@ -497,13 +497,14 @@ kubectl get virtualmcpserver engineering-vmcp -n toolhive-system \ **Possible causes and solutions:** -1. **MCPGroup not in Ready state** +1. **MCPGroup hasn't checked its members yet** ```bash kubectl get mcpgroup my-group -n toolhive-system ``` - Wait for the group to reach Ready state before starting vMCP. + Wait for the `Checked` column to report `True` (the `MCPServersChecked` + condition on the MCPGroup status) before starting vMCP. 2. **Backend resources not referencing the correct group** diff --git a/docs/toolhive/guides-vmcp/configuration.mdx b/docs/toolhive/guides-vmcp/configuration.mdx index 02eace92..5613e549 100644 --- a/docs/toolhive/guides-vmcp/configuration.mdx +++ b/docs/toolhive/guides-vmcp/configuration.mdx @@ -26,9 +26,10 @@ spec: description: Group of backend MCP servers for vMCP aggregation ``` -The MCPGroup must exist in the same namespace as your VirtualMCPServer and be in -a Ready state before the VirtualMCPServer can start. Backend resources reference -this group using the `groupRef` field in their spec. +The MCPGroup must exist in the same namespace as your VirtualMCPServer and have +its `MCPServersChecked` condition reported as `True` before the VirtualMCPServer +can start. Backend resources reference this group using the `groupRef` field in +their spec. ## Add backends to a group @@ -130,12 +131,15 @@ spec: type: anonymous # Disables authentication; do not use in production ``` -The MCPGroup must exist in the same namespace and be in a Ready state before the -VirtualMCPServer can start. By default, vMCP automatically discovers and -aggregates all MCPServer, MCPRemoteProxy, and MCPServerEntry resources in the -referenced group. You can also define backends explicitly in the configuration -(inline mode). See [Backend discovery modes](./backend-discovery.mdx) for -details on both approaches. +The MCPGroup must exist in the same namespace, and its `MCPServersChecked` +condition must be `True` before the VirtualMCPServer can start. You can verify +this with `kubectl get mcpgroup` and look for `True` under the `Checked` column. + +By default, vMCP automatically discovers and aggregates all MCPServer, +MCPRemoteProxy, and MCPServerEntry resources in the referenced group. You can +also define backends explicitly in the configuration (inline mode). See +[Backend discovery modes](./backend-discovery.mdx) for details on both +approaches. ## Configure authentication diff --git a/docs/toolhive/guides-vmcp/quickstart.mdx b/docs/toolhive/guides-vmcp/quickstart.mdx index 1c947be6..ea675773 100644 --- a/docs/toolhive/guides-vmcp/quickstart.mdx +++ b/docs/toolhive/guides-vmcp/quickstart.mdx @@ -269,8 +269,9 @@ backends into a single endpoint.
VirtualMCPServer stuck in Pending -The vMCP pod won't become Ready until the MCPGroup is in `Ready` state and at -least one backend MCPServer is `Ready`. Check both: +The vMCP pod won't become Ready until the MCPGroup's `Checked` column reports +`True` (its `MCPServersChecked` condition) and at least one backend MCPServer is +`Ready`. Check both: ```bash kubectl get mcpgroups,mcpservers -n toolhive-system diff --git a/docs/toolhive/reference/cli/thv.md b/docs/toolhive/reference/cli/thv.md index bc0975da..2749ca76 100644 --- a/docs/toolhive/reference/cli/thv.md +++ b/docs/toolhive/reference/cli/thv.md @@ -42,6 +42,7 @@ thv [flags] * [thv group](thv_group.md) - Manage logical groupings of MCP servers * [thv inspector](thv_inspector.md) - Launches the MCP Inspector UI and connects it to the specified MCP server * [thv list](thv_list.md) - List running MCP servers +* [thv llm](thv_llm.md) - Manage LLM gateway authentication * [thv logs](thv_logs.md) - Output the logs of an MCP server or manage log files * [thv mcp](thv_mcp.md) - Interact with MCP servers for debugging * [thv proxy](thv_proxy.md) - Create a transparent proxy for an MCP server with authentication support diff --git a/docs/toolhive/reference/cli/thv_llm.md b/docs/toolhive/reference/cli/thv_llm.md new file mode 100644 index 00000000..b39471c6 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm.md @@ -0,0 +1,57 @@ +--- +title: thv llm +hide_title: true +description: Reference for ToolHive CLI command `thv llm` +last_update: + author: autogenerated +slug: thv_llm +mdx: + format: md +--- + +## thv llm + +Manage LLM gateway authentication + +### Synopsis + +Configure and manage authentication for OIDC-protected LLM gateways. + +The llm command bridges AI coding tools to LLM gateways by handling OIDC +authentication transparently. Two modes are supported: + + Proxy mode — a localhost reverse proxy injects fresh tokens for tools + that only accept static API keys (e.g. Cursor). + Token helper — "thv llm token" prints a fresh JWT suitable for use as + apiKeyHelper or auth.command in OIDC-capable tools + (e.g. Claude Code). + +To configure the gateway connection settings, use: + + thv llm config set --gateway-url https://llm.example.com \ + --issuer https://auth.example.com \ + --client-id my-client-id + +Use "thv llm config show" to view the current configuration. + +### Options + +``` + -h, --help help for llm +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv](thv.md) - ToolHive (thv) is a lightweight, secure, and fast manager for MCP servers +* [thv llm config](thv_llm_config.md) - Manage LLM gateway configuration +* [thv llm proxy](thv_llm_proxy.md) - Manage the LLM gateway localhost proxy +* [thv llm setup](thv_llm_setup.md) - Configure detected AI tools to use the LLM gateway +* [thv llm teardown](thv_llm_teardown.md) - Remove LLM gateway configuration from all (or one) configured tools +* [thv llm token](thv_llm_token.md) - Print a fresh LLM gateway access token to stdout + diff --git a/docs/toolhive/reference/cli/thv_llm_config.md b/docs/toolhive/reference/cli/thv_llm_config.md new file mode 100644 index 00000000..bd8e6543 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_config.md @@ -0,0 +1,38 @@ +--- +title: thv llm config +hide_title: true +description: Reference for ToolHive CLI command `thv llm config` +last_update: + author: autogenerated +slug: thv_llm_config +mdx: + format: md +--- + +## thv llm config + +Manage LLM gateway configuration + +### Synopsis + +The config command provides subcommands to manage LLM gateway connection settings. + +### Options + +``` + -h, --help help for config +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm](thv_llm.md) - Manage LLM gateway authentication +* [thv llm config reset](thv_llm_config_reset.md) - Clear all LLM gateway configuration and cached tokens +* [thv llm config set](thv_llm_config_set.md) - Set LLM gateway connection settings +* [thv llm config show](thv_llm_config_show.md) - Display current LLM gateway configuration + diff --git a/docs/toolhive/reference/cli/thv_llm_config_reset.md b/docs/toolhive/reference/cli/thv_llm_config_reset.md new file mode 100644 index 00000000..9b5402f9 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_config_reset.md @@ -0,0 +1,40 @@ +--- +title: thv llm config reset +hide_title: true +description: Reference for ToolHive CLI command `thv llm config reset` +last_update: + author: autogenerated +slug: thv_llm_config_reset +mdx: + format: md +--- + +## thv llm config reset + +Clear all LLM gateway configuration and cached tokens + +### Synopsis + +Remove all LLM gateway settings from config.yaml and delete cached OIDC +tokens from the secrets provider. + +``` +thv llm config reset [flags] +``` + +### Options + +``` + -h, --help help for reset +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm config](thv_llm_config.md) - Manage LLM gateway configuration + diff --git a/docs/toolhive/reference/cli/thv_llm_config_set.md b/docs/toolhive/reference/cli/thv_llm_config_set.md new file mode 100644 index 00000000..2d5a89af --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_config_set.md @@ -0,0 +1,52 @@ +--- +title: thv llm config set +hide_title: true +description: Reference for ToolHive CLI command `thv llm config set` +last_update: + author: autogenerated +slug: thv_llm_config_set +mdx: + format: md +--- + +## thv llm config set + +Set LLM gateway connection settings + +### Synopsis + +Persist LLM gateway connection settings to config.yaml. + +Example: + thv llm config set \ + --gateway-url https://llm.example.com \ + --issuer https://auth.example.com \ + --client-id my-client-id + +``` +thv llm config set [flags] +``` + +### Options + +``` + --audience string OIDC audience (optional) + --callback-port int OIDC callback port (omit to keep current; default: ephemeral) + --client-id string OIDC client ID + --gateway-url string LLM gateway base URL (must use HTTPS) + -h, --help help for set + --issuer string OIDC issuer URL + --proxy-port int Localhost proxy listen port (omit to keep current; default: 14000) + --tls-skip-verify Skip TLS certificate verification for the upstream gateway (local dev only; use --tls-skip-verify=false to clear) +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm config](thv_llm_config.md) - Manage LLM gateway configuration + diff --git a/docs/toolhive/reference/cli/thv_llm_config_show.md b/docs/toolhive/reference/cli/thv_llm_config_show.md new file mode 100644 index 00000000..f1e48b7c --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_config_show.md @@ -0,0 +1,36 @@ +--- +title: thv llm config show +hide_title: true +description: Reference for ToolHive CLI command `thv llm config show` +last_update: + author: autogenerated +slug: thv_llm_config_show +mdx: + format: md +--- + +## thv llm config show + +Display current LLM gateway configuration + +``` +thv llm config show [flags] +``` + +### Options + +``` + --format string Output format (json, text) (default "text") + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm config](thv_llm_config.md) - Manage LLM gateway configuration + diff --git a/docs/toolhive/reference/cli/thv_llm_proxy.md b/docs/toolhive/reference/cli/thv_llm_proxy.md new file mode 100644 index 00000000..bdb8a3f3 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_proxy.md @@ -0,0 +1,32 @@ +--- +title: thv llm proxy +hide_title: true +description: Reference for ToolHive CLI command `thv llm proxy` +last_update: + author: autogenerated +slug: thv_llm_proxy +mdx: + format: md +--- + +## thv llm proxy + +Manage the LLM gateway localhost proxy + +### Options + +``` + -h, --help help for proxy +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm](thv_llm.md) - Manage LLM gateway authentication +* [thv llm proxy start](thv_llm_proxy_start.md) - Start the LLM gateway localhost proxy + diff --git a/docs/toolhive/reference/cli/thv_llm_proxy_start.md b/docs/toolhive/reference/cli/thv_llm_proxy_start.md new file mode 100644 index 00000000..77b9e58e --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_proxy_start.md @@ -0,0 +1,46 @@ +--- +title: thv llm proxy start +hide_title: true +description: Reference for ToolHive CLI command `thv llm proxy start` +last_update: + author: autogenerated +slug: thv_llm_proxy_start +mdx: + format: md +--- + +## thv llm proxy start + +Start the LLM gateway localhost proxy + +### Synopsis + +Start a localhost reverse proxy that injects fresh OIDC tokens for AI tools +that only accept static API keys (e.g. Cursor). + +The proxy runs in the foreground and blocks until interrupted (Ctrl+C). +To run it in the background, use your shell or a process manager: + + thv llm proxy start & + +``` +thv llm proxy start [flags] +``` + +### Options + +``` + -h, --help help for start + --tls-skip-verify Skip TLS certificate verification for the upstream gateway (overrides stored config; local dev only) +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm proxy](thv_llm_proxy.md) - Manage the LLM gateway localhost proxy + diff --git a/docs/toolhive/reference/cli/thv_llm_setup.md b/docs/toolhive/reference/cli/thv_llm_setup.md new file mode 100644 index 00000000..95d19b01 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_setup.md @@ -0,0 +1,64 @@ +--- +title: thv llm setup +hide_title: true +description: Reference for ToolHive CLI command `thv llm setup` +last_update: + author: autogenerated +slug: thv_llm_setup +mdx: + format: md +--- + +## thv llm setup + +Configure detected AI tools to use the LLM gateway + +### Synopsis + +Detect installed AI coding tools (Claude Code, Gemini CLI, Cursor, VS Code, +Xcode) and patch each tool's configuration to route through the LLM gateway. + +Token-helper tools (Claude Code, Gemini CLI) are configured to call +"thv llm token" to obtain a fresh OIDC token on demand. + +Proxy-mode tools (Cursor, VS Code, Xcode) are configured to send requests to +the localhost reverse proxy started by "thv llm proxy start". + +Use --client to configure only a single named tool instead of all detected +tools. An error is returned if the named client is not installed. + +Inline flags (--gateway-url, --issuer, --client-id, etc.) are applied for this +run and persisted to config only after login and tool patching succeed. This +lets you combine "config set" and "setup" into a single command. + +Run "thv llm teardown" to revert all changes. + +``` +thv llm setup [flags] +``` + +### Options + +``` + --anthropic-path-prefix string Path prefix appended to the gateway URL when writing ANTHROPIC_BASE_URL for direct-mode tools (e.g. /anthropic). When omitted, the gateway is probed automatically. + --audience string OIDC audience (optional) + --callback-port int OIDC callback port (omit to keep current; default: ephemeral) + --client string Configure only this AI tool by name (e.g. claude-code, cursor). Omit to configure all detected tools. + --client-id string OIDC client ID + --gateway-url string LLM gateway base URL (must use HTTPS) + -h, --help help for setup + --issuer string OIDC issuer URL + --proxy-port int Localhost proxy listen port (omit to keep current; default: 14000) + --tls-skip-verify Skip TLS certificate verification for the upstream gateway (local dev only). For direct-mode tools (Claude Code, Gemini CLI) this sets NODE_TLS_REJECT_UNAUTHORIZED=0, disabling TLS for ALL of that tool's outbound connections. For proxy-mode tools only the proxy-to-gateway connection is affected. +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm](thv_llm.md) - Manage LLM gateway authentication + diff --git a/docs/toolhive/reference/cli/thv_llm_teardown.md b/docs/toolhive/reference/cli/thv_llm_teardown.md new file mode 100644 index 00000000..5a91facb --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_teardown.md @@ -0,0 +1,45 @@ +--- +title: thv llm teardown +hide_title: true +description: Reference for ToolHive CLI command `thv llm teardown` +last_update: + author: autogenerated +slug: thv_llm_teardown +mdx: + format: md +--- + +## thv llm teardown + +Remove LLM gateway configuration from all (or one) configured tools + +### Synopsis + +Revert the configuration changes made by "thv llm setup" for all configured +tools, or for a single tool when tool-name is provided as a positional argument +or via --client. + +Use --purge-tokens to also remove cached OIDC tokens from the secrets provider. + +``` +thv llm teardown [tool-name] [flags] +``` + +### Options + +``` + --client string Remove configuration for only this AI tool by name (e.g. claude-code, cursor). Omit to revert all configured tools. + -h, --help help for teardown + --purge-tokens Also delete cached OIDC tokens from the secrets provider +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm](thv_llm.md) - Manage LLM gateway authentication + diff --git a/docs/toolhive/reference/cli/thv_llm_token.md b/docs/toolhive/reference/cli/thv_llm_token.md new file mode 100644 index 00000000..b5a175b9 --- /dev/null +++ b/docs/toolhive/reference/cli/thv_llm_token.md @@ -0,0 +1,41 @@ +--- +title: thv llm token +hide_title: true +description: Reference for ToolHive CLI command `thv llm token` +last_update: + author: autogenerated +slug: thv_llm_token +mdx: + format: md +--- + +## thv llm token + +Print a fresh LLM gateway access token to stdout + +### Synopsis + +Print a fresh OIDC access token to stdout (all other output on stderr). +Intended for use as apiKeyHelper or auth.command in OIDC-capable AI tools. +Runs non-interactively — will not launch a browser flow. + +``` +thv llm token [flags] +``` + +### Options + +``` + -h, --help help for token +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv llm](thv_llm.md) - Manage LLM gateway authentication + diff --git a/docs/toolhive/reference/cli/thv_serve.md b/docs/toolhive/reference/cli/thv_serve.md index c4551426..e7a0c95d 100644 --- a/docs/toolhive/reference/cli/thv_serve.md +++ b/docs/toolhive/reference/cli/thv_serve.md @@ -41,7 +41,7 @@ thv serve [flags] --sentry-dsn string Sentry DSN for error tracking and distributed tracing (falls back to SENTRY_DSN env var) --sentry-environment string Sentry environment name, e.g. production or development (falls back to SENTRY_ENVIRONMENT env var) --sentry-traces-sample-rate float Sentry traces sample rate (0.0-1.0) for performance monitoring (default 1) - --socket string UNIX socket path to bind the server to (overrides host and port if provided) + --socket string UNIX socket path or, on Windows, a named pipe (\\.\pipe\) to bind the server to (overrides host and port if provided) ``` ### Options inherited from parent commands diff --git a/docs/toolhive/reference/crds/index.mdx b/docs/toolhive/reference/crds/index.mdx index e6058455..a583d676 100644 --- a/docs/toolhive/reference/crds/index.mdx +++ b/docs/toolhive/reference/crds/index.mdx @@ -130,4 +130,13 @@ references. }} /> + + diff --git a/docs/toolhive/reference/crds/mcpwebhookconfig.mdx b/docs/toolhive/reference/crds/mcpwebhookconfig.mdx new file mode 100644 index 00000000..5bc9a067 --- /dev/null +++ b/docs/toolhive/reference/crds/mcpwebhookconfig.mdx @@ -0,0 +1,27 @@ +--- +title: MCPWebhookConfig +description: >- + Schema reference for MCPWebhookConfig, which configures validating and mutating webhook middleware for MCP servers. +displayed_sidebar: toolhiveSidebar +toc_max_heading_level: 4 +--- + +`MCPWebhookConfig` defines validating and mutating webhook middleware that intercepts MCP tool calls. [MCPServer](./mcpserver.mdx) references an `MCPWebhookConfig` via `spec.webhookConfigRef` to delegate tool-call authorization or request transformation to an external HTTPS service. This is the Kubernetes equivalent of the CLI [`--webhook-config`](../../guides-cli/webhooks.mdx) flag, configured declaratively as a custom resource. + +**API:** `toolhive.stacklok.dev/v1alpha1` + · **Scope:** Namespaced · **Short names:** `mwc` + +## Example + +```yaml title="mcpwebhookconfig.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPWebhookConfig +metadata: + name: my-mcpwebhookconfig + namespace: default +spec: {} +``` + +## Schema + + diff --git a/scripts/lib/crd-intros.mjs b/scripts/lib/crd-intros.mjs index f8a12ea1..5aa71ea0 100644 --- a/scripts/lib/crd-intros.mjs +++ b/scripts/lib/crd-intros.mjs @@ -151,4 +151,13 @@ export const intros = { intro: '`EmbeddingServer` defines a containerized embedding model server managed by the ToolHive operator. The [VirtualMCPServer](./virtualmcpserver.mdx) optimizer references an `EmbeddingServer` to generate vector embeddings for tool discovery.', }, + MCPWebhookConfig: { + slug: 'mcpwebhookconfig', + group: 'shared', + summary: 'Delegate MCP tool authorization to an external webhook.', + description: + 'Schema reference for MCPWebhookConfig, which configures validating and mutating webhook middleware for MCP servers.', + intro: + '`MCPWebhookConfig` defines validating and mutating webhook middleware that intercepts MCP tool calls. [MCPServer](./mcpserver.mdx) references an `MCPWebhookConfig` via `spec.webhookConfigRef` to delegate tool-call authorization or request transformation to an external HTTPS service. This is the Kubernetes equivalent of the CLI [`--webhook-config`](../../guides-cli/webhooks.mdx) flag, configured declaratively as a custom resource.', + }, }; diff --git a/src/components/CRDReference/schemas.ts b/src/components/CRDReference/schemas.ts index e2ef7925..0f591233 100644 --- a/src/components/CRDReference/schemas.ts +++ b/src/components/CRDReference/schemas.ts @@ -12,6 +12,7 @@ import MCPServerEntry from '@site/static/api-specs/crds/mcpserverentries.schema. import MCPServer from '@site/static/api-specs/crds/mcpservers.schema.json'; import MCPTelemetryConfig from '@site/static/api-specs/crds/mcptelemetryconfigs.schema.json'; import MCPToolConfig from '@site/static/api-specs/crds/mcptoolconfigs.schema.json'; +import MCPWebhookConfig from '@site/static/api-specs/crds/mcpwebhookconfigs.schema.json'; import VirtualMCPCompositeToolDefinition from '@site/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json'; import VirtualMCPServer from '@site/static/api-specs/crds/virtualmcpservers.schema.json'; @@ -26,6 +27,7 @@ export const schemas = { MCPServer, MCPTelemetryConfig, MCPToolConfig, + MCPWebhookConfig, VirtualMCPCompositeToolDefinition, VirtualMCPServer, } as const; diff --git a/static/api-specs/crds/index.json b/static/api-specs/crds/index.json index df6b84a2..c1cb578f 100644 --- a/static/api-specs/crds/index.json +++ b/static/api-specs/crds/index.json @@ -269,6 +269,12 @@ "paths": [ "spec.toolConfigRef" ] + }, + { + "targetKind": "MCPWebhookConfig", + "paths": [ + "spec.webhookConfigRef" + ] } ], "referencedBy": [], @@ -341,6 +347,27 @@ ], "slug": "mcptoolconfig" }, + { + "kind": "MCPWebhookConfig", + "plural": "mcpwebhookconfigs", + "group": "toolhive.stacklok.dev", + "version": "v1alpha1", + "shortNames": [ + "mwc" + ], + "scope": "Namespaced", + "description": "MCPWebhookConfig is the Schema for the mcpwebhookconfigs API.", + "references": [], + "referencedBy": [ + { + "sourceKind": "MCPServer", + "paths": [ + "spec.webhookConfigRef" + ] + } + ], + "slug": "mcpwebhookconfig" + }, { "kind": "VirtualMCPCompositeToolDefinition", "plural": "virtualmcpcompositetooldefinitions", diff --git a/static/api-specs/crds/mcpexternalauthconfigs.schema.json b/static/api-specs/crds/mcpexternalauthconfigs.schema.json index 0a8fe503..e9648673 100644 --- a/static/api-specs/crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/crds/mcpexternalauthconfigs.schema.json @@ -212,7 +212,7 @@ "type": "object" }, "usernameSecretRef": { - "description": "UsernameSecretRef references a Secret containing the Redis ACL username.", + "description": "UsernameSecretRef references a Secret containing the Redis ACL username.\nWhen omitted, connections use legacy password-only AUTH. Omit for managed\nRedis tiers that do not support ACL users (e.g. GCP Memorystore Basic/Standard\nHA, Azure Cache for Redis). Set for services that support ACL users (e.g. AWS\nElastiCache non-cluster with Redis 6+ RBAC).", "properties": { "key": { "description": "Key is the key within the secret", @@ -231,11 +231,18 @@ } }, "required": [ - "passwordSecretRef", - "usernameSecretRef" + "passwordSecretRef" ], "type": "object" }, + "addr": { + "description": "Addr is the Redis server address (host:port). Required for standalone and cluster modes.\nUse for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,\nAWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).\nMutually exclusive with sentinelConfig.", + "type": "string" + }, + "clusterMode": { + "description": "ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a\nRedis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache\ncluster mode enabled). Requires addr to be set.", + "type": "boolean" + }, "dialTimeout": { "default": "5s", "description": "DialTimeout is the timeout for establishing connections.\nFormat: Go duration string (e.g., \"5s\", \"1m\").", @@ -249,7 +256,7 @@ "type": "string" }, "sentinelConfig": { - "description": "SentinelConfig holds Redis Sentinel configuration.", + "description": "SentinelConfig holds Redis Sentinel configuration.\nUse for self-managed Redis with Sentinel-based HA. Mutually exclusive with addr.", "properties": { "db": { "default": 0, @@ -299,7 +306,7 @@ "type": "object" }, "sentinelTls": { - "description": "SentinelTLS configures TLS for connections to Sentinel instances.\nPresence of this field enables TLS. Omit to use plaintext.\nWhen omitted, sentinel connections use plaintext (no fallback to TLS config).", + "description": "SentinelTLS configures TLS for connections to Sentinel instances.\nOnly applies when sentinelConfig is set. Presence of this field enables TLS.", "properties": { "caCertSecretRef": { "description": "CACertSecretRef references a Secret containing a PEM-encoded CA certificate\nfor verifying the server. When not specified, system root CAs are used.", @@ -362,10 +369,19 @@ } }, "required": [ - "aclUserConfig", - "sentinelConfig" + "aclUserConfig" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of addr or sentinelConfig must be set", + "rule": "(has(self.addr) && self.addr.size() > 0) != has(self.sentinelConfig)" + }, + { + "message": "clusterMode requires addr to be set", + "rule": "!(has(self.clusterMode) && self.clusterMode) || (has(self.addr) && self.addr.size() > 0)" + } + ] }, "type": { "default": "memory", @@ -403,7 +419,7 @@ "upstreamProviders": { "description": "UpstreamProviders configures connections to upstream Identity Providers.\nThe embedded auth server delegates authentication to these providers.\nMCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple.", "items": { - "description": "UpstreamProviderConfig defines configuration for an upstream Identity Provider.", + "description": "UpstreamProviderConfig defines configuration for an upstream Identity Provider.\n\nExactly one of OIDCConfig or OAuth2Config must be set and must match the\ndeclared Type: oidc-typed providers set OIDCConfig, oauth2-typed providers\nset OAuth2Config. The CEL rule below enforces the pairing at admission; the\nmatching Go-level check in validateUpstreamProvider provides defense-in-depth\nfor stored objects.\n\nThe rule is structured as a chain of equality checks ending in an explicit\n`false`, so adding a new UpstreamProviderType value without extending this\nrule fails admission instead of silently demanding the OAuth2 shape. When\nadding a new type, extend both this rule and validateUpstreamProvider.", "properties": { "name": { "description": "Name uniquely identifies this upstream provider.\nUsed for routing decisions and session binding in multi-upstream scenarios.\nMust be lowercase alphanumeric with hyphens (DNS-label-like).", @@ -429,7 +445,7 @@ "type": "string" }, "clientId": { - "description": "ClientID is the OAuth 2.0 client identifier registered with the upstream IDP.", + "description": "ClientID is the OAuth 2.0 client identifier registered with the upstream IDP.\nMutually exclusive with DCRConfig: when DCRConfig is set, ClientID is obtained\nat runtime via RFC 7591 Dynamic Client Registration and must be left empty.", "type": "string" }, "clientSecretRef": { @@ -450,6 +466,58 @@ ], "type": "object" }, + "dcrConfig": { + "description": "DCRConfig enables RFC 7591 Dynamic Client Registration against the upstream\nauthorization server. When set, the client credentials are obtained at\nruntime rather than being pre-provisioned, and ClientID must be left empty.\nMutually exclusive with ClientID.", + "properties": { + "discoveryUrl": { + "description": "DiscoveryURL is the RFC 8414 / OIDC Discovery document URL. The resolver\nissues a single GET against this URL (no well-known-path fallback) and\nreads registration_endpoint, authorization_endpoint, token_endpoint,\ntoken_endpoint_auth_methods_supported, and scopes_supported from the\nresponse.\nMutually exclusive with RegistrationEndpoint.\nHTTPS is required because the registration endpoint resolved from this\ndocument carries the initial access token and the issued client_secret\n(RFC 7591 §3, RFC 8414 §3). MaxLength is a defensive size cap (etcd\nobject budget, regex evaluation cost) and matches the conventional URL\nlength cap.", + "maxLength": 2048, + "pattern": "^https://[^\\s?#]+[^/\\s?#]$", + "type": "string" + }, + "initialAccessTokenRef": { + "description": "InitialAccessTokenRef is an optional reference to a Kubernetes Secret\ncarrying an RFC 7591 §3 initial access token. When set, the resolver\npresents the token value as a Bearer credential on the registration\nrequest. Mirrors the ClientSecretRef pattern.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "registrationEndpoint": { + "description": "RegistrationEndpoint is the RFC 7591 registration endpoint URL used\ndirectly, bypassing discovery. When using this field, the caller is\nexpected to also supply AuthorizationEndpoint, TokenEndpoint, and an\nexplicit Scopes list on the parent OAuth2UpstreamConfig.\nMutually exclusive with DiscoveryURL.\nHTTPS is required because the registration endpoint carries the initial\naccess token and the issued client_secret (RFC 7591 §3, RFC 8414 §3).\nMaxLength is a defensive size cap (etcd object budget, regex evaluation\ncost) and matches the conventional URL length cap.", + "maxLength": 2048, + "pattern": "^https://[^\\s?#]+[^/\\s?#]$", + "type": "string" + }, + "softwareId": { + "description": "SoftwareID is the RFC 7591 \"software_id\" registration metadata value,\nidentifying the client software independent of any particular\nregistration instance. Typically a UUID or short identifier.", + "maxLength": 255, + "type": "string" + }, + "softwareStatement": { + "description": "SoftwareStatement is the RFC 7591 \"software_statement\" JWT asserting\nmetadata about the client software, signed by a party the authorization\nserver trusts.\n\nStored inline on the CR. The JWT is signed but not encrypted, so its\ncontents are visible to anyone with get/list/watch on this resource and\nappear in etcd backups in plaintext. Treat the value as non-confidential\n(signed attestation, not a secret). Operators that rotate software\nstatements like bearer credentials should keep them at the authorization\nserver side and rely on the registration endpoint's initial access\ntoken (see InitialAccessTokenRef) instead of placing them on the CR.\n\nBounded to 16384 characters as a defensive size cap (etcd object\nbudget, regex evaluation cost). Real-world signed statements with\nembedded x5c certificate chains, JWKS keys, or OIDC-Federation\ntrust-framework metadata routinely exceed 4 KB.", + "maxLength": 16384, + "type": "string" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of discoveryUrl or registrationEndpoint must be set", + "rule": "has(self.discoveryUrl) != has(self.registrationEndpoint)" + } + ] + }, "redirectUri": { "description": "RedirectURI is the callback URL where the upstream IDP will redirect after authentication.\nWhen not specified, defaults to `{resourceUrl}/oauth/callback` where `resourceUrl` is the\nURL associated with the resource (e.g., MCPServer or vMCP) using this config.", "type": "string" @@ -555,10 +623,19 @@ }, "required": [ "authorizationEndpoint", - "clientId", "tokenEndpoint" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of clientId or dcrConfig must be set", + "rule": "(has(self.clientId) && size(self.clientId) > 0) ? !has(self.dcrConfig) : has(self.dcrConfig)" + }, + { + "message": "clientSecretRef must not be set when dcrConfig is set; the client_secret is obtained at runtime via Dynamic Client Registration", + "rule": "!(has(self.dcrConfig) && has(self.clientSecretRef))" + } + ] }, "oidcConfig": { "description": "OIDCConfig contains OIDC-specific configuration.\nRequired when Type is \"oidc\", must be nil when Type is \"oauth2\".", @@ -689,7 +766,13 @@ "name", "type" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "type must be 'oidc' or 'oauth2'; oidcConfig must be set when type is 'oidc' and oauth2Config must be set when type is 'oauth2' (and the other must not be set)", + "rule": "self.type == 'oidc' ? (has(self.oidcConfig) && !has(self.oauth2Config)) : self.type == 'oauth2' ? (has(self.oauth2Config) && !has(self.oidcConfig)) : false" + } + ] }, "minItems": 1, "type": "array", diff --git a/static/api-specs/crds/mcpremoteproxies.schema.json b/static/api-specs/crds/mcpremoteproxies.schema.json index a5587643..3c0ca6c5 100644 --- a/static/api-specs/crds/mcpremoteproxies.schema.json +++ b/static/api-specs/crds/mcpremoteproxies.schema.json @@ -86,6 +86,13 @@ "minItems": 1, "type": "array", "x-kubernetes-list-type": "atomic" + }, + "primaryUpstreamProvider": { + "description": "PrimaryUpstreamProvider names the upstream IDP whose access token's claims\nCedar should evaluate. Currently honored only when the parent\nAuthzConfigRef.Type is \"inline\"; configMap-sourced policies will support\nthis in a future release (see #5208). Only meaningful for VirtualMCPServer\nwith an embedded auth server. When empty and an embedded auth server has\nupstreams configured, the controller defaults to the first upstream\nprovider. The name must match one of the upstreams declared on\nspec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is\nrejected with AuthServerConfigValidated=False. MCPServer and MCPRemoteProxy\nhave no embedded auth server; setting this field on those CRs surfaces an\nAuthzPrimaryUpstreamProviderIgnored advisory condition on the resource.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" } }, "required": [ diff --git a/static/api-specs/crds/mcpservers.schema.json b/static/api-specs/crds/mcpservers.schema.json index 395fb731..a8ce366a 100644 --- a/static/api-specs/crds/mcpservers.schema.json +++ b/static/api-specs/crds/mcpservers.schema.json @@ -94,6 +94,13 @@ "minItems": 1, "type": "array", "x-kubernetes-list-type": "atomic" + }, + "primaryUpstreamProvider": { + "description": "PrimaryUpstreamProvider names the upstream IDP whose access token's claims\nCedar should evaluate. Currently honored only when the parent\nAuthzConfigRef.Type is \"inline\"; configMap-sourced policies will support\nthis in a future release (see #5208). Only meaningful for VirtualMCPServer\nwith an embedded auth server. When empty and an embedded auth server has\nupstreams configured, the controller defaults to the first upstream\nprovider. The name must match one of the upstreams declared on\nspec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is\nrejected with AuthServerConfigValidated=False. MCPServer and MCPRemoteProxy\nhave no embedded auth server; setting this field on those CRs surfaces an\nAuthzPrimaryUpstreamProviderIgnored advisory condition on the resource.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" } }, "required": [ @@ -731,6 +738,19 @@ "name" ], "x-kubernetes-list-type": "map" + }, + "webhookConfigRef": { + "description": "WebhookConfigRef references a MCPWebhookConfig resource for webhook middleware configuration.\nThe referenced MCPWebhookConfig must exist in the same namespace as this MCPServer.", + "properties": { + "name": { + "description": "Name is the name of the MCPWebhookConfig resource", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" } }, "required": [ @@ -862,6 +882,10 @@ "url": { "description": "URL is the URL where the MCP server can be accessed", "type": "string" + }, + "webhookConfigHash": { + "description": "WebhookConfigHash is the hash of the referenced MCPWebhookConfig spec", + "type": "string" } }, "type": "object" diff --git a/static/api-specs/crds/mcpwebhookconfigs.example.yaml b/static/api-specs/crds/mcpwebhookconfigs.example.yaml new file mode 100644 index 00000000..079e3c83 --- /dev/null +++ b/static/api-specs/crds/mcpwebhookconfigs.example.yaml @@ -0,0 +1,6 @@ +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPWebhookConfig +metadata: + name: my-mcpwebhookconfig + namespace: default +spec: {} diff --git a/static/api-specs/crds/mcpwebhookconfigs.schema.json b/static/api-specs/crds/mcpwebhookconfigs.schema.json new file mode 100644 index 00000000..2e006ed3 --- /dev/null +++ b/static/api-specs/crds/mcpwebhookconfigs.schema.json @@ -0,0 +1,376 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MCPWebhookConfig", + "description": "MCPWebhookConfig is the Schema for the mcpwebhookconfigs API.", + "x-kubernetes-group": "toolhive.stacklok.dev", + "x-kubernetes-kind": "MCPWebhookConfig", + "x-kubernetes-version": "v1alpha1", + "x-kubernetes-plural": "mcpwebhookconfigs", + "x-kubernetes-short-names": [ + "mwc" + ], + "x-kubernetes-scope": "Namespaced", + "properties": { + "spec": { + "description": "MCPWebhookConfigSpec defines the desired state of MCPWebhookConfig", + "properties": { + "mutating": { + "description": "Mutating webhooks are called to transform MCP requests before processing.", + "items": { + "description": "WebhookSpec defines the configuration for a single webhook middleware", + "properties": { + "failurePolicy": { + "default": "fail", + "description": "FailurePolicy defines how to handle errors when communicating with the webhook.\nSupported values: \"fail\", \"ignore\". Defaults to \"fail\".", + "enum": [ + "fail", + "ignore" + ], + "type": "string" + }, + "hmacSecretRef": { + "description": "HMACSecretRef references a Kubernetes Secret containing the HMAC signing key\nused to sign the webhook payload. If set, the X-Toolhive-Signature header will be injected.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "name": { + "description": "Name is a unique identifier for this webhook", + "maxLength": 63, + "minLength": 1, + "type": "string" + }, + "timeout": { + "description": "Timeout configures the maximum time to wait for the webhook to respond.\nDefaults to 10s if not specified. Maximum is 30s.", + "format": "duration", + "type": "string" + }, + "tlsConfig": { + "description": "TLSConfig contains optional TLS configuration for the webhook connection.", + "properties": { + "caSecretRef": { + "description": "CASecretRef references a Secret containing the CA certificate bundle used to verify the webhook server's certificate.\nContains a bundle of PEM-encoded X.509 certificates.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "clientCertSecretRef": { + "description": "ClientCertSecretRef references a Secret containing the client certificate for mTLS authentication.\nThe referenced key must contain a PEM-encoded client certificate.\nUse ClientKeySecretRef to provide the corresponding private key.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "clientKeySecretRef": { + "description": "ClientKeySecretRef references a Secret containing the private key for the client certificate.\nRequired when ClientCertSecretRef is set to enable mTLS.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "insecureSkipVerify": { + "description": "InsecureSkipVerify disables server certificate verification.\nWARNING: This should only be used for development/testing and not in production environments.", + "type": "boolean" + } + }, + "type": "object" + }, + "url": { + "description": "URL is the endpoint to call for this webhook. Must be an HTTP/HTTPS URL.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "url" + ], + "type": "object" + }, + "type": "array" + }, + "validating": { + "description": "Validating webhooks are called to approve or deny MCP requests.", + "items": { + "description": "WebhookSpec defines the configuration for a single webhook middleware", + "properties": { + "failurePolicy": { + "default": "fail", + "description": "FailurePolicy defines how to handle errors when communicating with the webhook.\nSupported values: \"fail\", \"ignore\". Defaults to \"fail\".", + "enum": [ + "fail", + "ignore" + ], + "type": "string" + }, + "hmacSecretRef": { + "description": "HMACSecretRef references a Kubernetes Secret containing the HMAC signing key\nused to sign the webhook payload. If set, the X-Toolhive-Signature header will be injected.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "name": { + "description": "Name is a unique identifier for this webhook", + "maxLength": 63, + "minLength": 1, + "type": "string" + }, + "timeout": { + "description": "Timeout configures the maximum time to wait for the webhook to respond.\nDefaults to 10s if not specified. Maximum is 30s.", + "format": "duration", + "type": "string" + }, + "tlsConfig": { + "description": "TLSConfig contains optional TLS configuration for the webhook connection.", + "properties": { + "caSecretRef": { + "description": "CASecretRef references a Secret containing the CA certificate bundle used to verify the webhook server's certificate.\nContains a bundle of PEM-encoded X.509 certificates.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "clientCertSecretRef": { + "description": "ClientCertSecretRef references a Secret containing the client certificate for mTLS authentication.\nThe referenced key must contain a PEM-encoded client certificate.\nUse ClientKeySecretRef to provide the corresponding private key.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "clientKeySecretRef": { + "description": "ClientKeySecretRef references a Secret containing the private key for the client certificate.\nRequired when ClientCertSecretRef is set to enable mTLS.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "insecureSkipVerify": { + "description": "InsecureSkipVerify disables server certificate verification.\nWARNING: This should only be used for development/testing and not in production environments.", + "type": "boolean" + } + }, + "type": "object" + }, + "url": { + "description": "URL is the endpoint to call for this webhook. Must be an HTTP/HTTPS URL.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "url" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one validating or mutating webhook must be defined", + "rule": "(has(self.validating) ? size(self.validating) : 0) + (has(self.mutating) ? size(self.mutating) : 0) > 0" + } + ] + }, + "status": { + "description": "MCPWebhookConfigStatus defines the observed state of MCPWebhookConfig", + "properties": { + "conditions": { + "description": "Conditions represent the latest available observations", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.", + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.", + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.", + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "configHash": { + "description": "ConfigHash is a hash of the spec, used for detecting changes", + "type": "string" + }, + "observedGeneration": { + "description": "ObservedGeneration is the last observed generation corresponding to the current status", + "format": "int64", + "type": "integer" + }, + "referencingWorkloads": { + "description": "ReferencingWorkloads is a list of workload resources that reference this MCPWebhookConfig.\nEach entry identifies the workload by kind and name.", + "items": { + "description": "WorkloadReference identifies a workload that references a shared configuration resource.\nNamespace is implicit — cross-namespace references are not supported.", + "properties": { + "kind": { + "description": "Kind is the type of workload resource", + "enum": [ + "MCPServer", + "VirtualMCPServer", + "MCPRemoteProxy" + ], + "type": "string" + }, + "name": { + "description": "Name is the name of the workload resource", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + } + }, + "type": "object" + } + }, + "type": "object" +} diff --git a/static/api-specs/crds/sidebar.json b/static/api-specs/crds/sidebar.json index 5a84af0c..f54915c9 100644 --- a/static/api-specs/crds/sidebar.json +++ b/static/api-specs/crds/sidebar.json @@ -32,7 +32,8 @@ "toolhive/reference/crds/mcptelemetryconfig", "toolhive/reference/crds/mcptoolconfig", "toolhive/reference/crds/virtualmcpcompositetooldefinition", - "toolhive/reference/crds/embeddingserver" + "toolhive/reference/crds/embeddingserver", + "toolhive/reference/crds/mcpwebhookconfig" ] } ] diff --git a/static/api-specs/crds/virtualmcpservers.schema.json b/static/api-specs/crds/virtualmcpservers.schema.json index 0a403ba4..a9c71e6b 100644 --- a/static/api-specs/crds/virtualmcpservers.schema.json +++ b/static/api-specs/crds/virtualmcpservers.schema.json @@ -103,7 +103,7 @@ "type": "object" }, "usernameSecretRef": { - "description": "UsernameSecretRef references a Secret containing the Redis ACL username.", + "description": "UsernameSecretRef references a Secret containing the Redis ACL username.\nWhen omitted, connections use legacy password-only AUTH. Omit for managed\nRedis tiers that do not support ACL users (e.g. GCP Memorystore Basic/Standard\nHA, Azure Cache for Redis). Set for services that support ACL users (e.g. AWS\nElastiCache non-cluster with Redis 6+ RBAC).", "properties": { "key": { "description": "Key is the key within the secret", @@ -122,11 +122,18 @@ } }, "required": [ - "passwordSecretRef", - "usernameSecretRef" + "passwordSecretRef" ], "type": "object" }, + "addr": { + "description": "Addr is the Redis server address (host:port). Required for standalone and cluster modes.\nUse for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,\nAWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).\nMutually exclusive with sentinelConfig.", + "type": "string" + }, + "clusterMode": { + "description": "ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a\nRedis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache\ncluster mode enabled). Requires addr to be set.", + "type": "boolean" + }, "dialTimeout": { "default": "5s", "description": "DialTimeout is the timeout for establishing connections.\nFormat: Go duration string (e.g., \"5s\", \"1m\").", @@ -140,7 +147,7 @@ "type": "string" }, "sentinelConfig": { - "description": "SentinelConfig holds Redis Sentinel configuration.", + "description": "SentinelConfig holds Redis Sentinel configuration.\nUse for self-managed Redis with Sentinel-based HA. Mutually exclusive with addr.", "properties": { "db": { "default": 0, @@ -190,7 +197,7 @@ "type": "object" }, "sentinelTls": { - "description": "SentinelTLS configures TLS for connections to Sentinel instances.\nPresence of this field enables TLS. Omit to use plaintext.\nWhen omitted, sentinel connections use plaintext (no fallback to TLS config).", + "description": "SentinelTLS configures TLS for connections to Sentinel instances.\nOnly applies when sentinelConfig is set. Presence of this field enables TLS.", "properties": { "caCertSecretRef": { "description": "CACertSecretRef references a Secret containing a PEM-encoded CA certificate\nfor verifying the server. When not specified, system root CAs are used.", @@ -253,10 +260,19 @@ } }, "required": [ - "aclUserConfig", - "sentinelConfig" + "aclUserConfig" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of addr or sentinelConfig must be set", + "rule": "(has(self.addr) && self.addr.size() > 0) != has(self.sentinelConfig)" + }, + { + "message": "clusterMode requires addr to be set", + "rule": "!(has(self.clusterMode) && self.clusterMode) || (has(self.addr) && self.addr.size() > 0)" + } + ] }, "type": { "default": "memory", @@ -294,7 +310,7 @@ "upstreamProviders": { "description": "UpstreamProviders configures connections to upstream Identity Providers.\nThe embedded auth server delegates authentication to these providers.\nMCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple.", "items": { - "description": "UpstreamProviderConfig defines configuration for an upstream Identity Provider.", + "description": "UpstreamProviderConfig defines configuration for an upstream Identity Provider.\n\nExactly one of OIDCConfig or OAuth2Config must be set and must match the\ndeclared Type: oidc-typed providers set OIDCConfig, oauth2-typed providers\nset OAuth2Config. The CEL rule below enforces the pairing at admission; the\nmatching Go-level check in validateUpstreamProvider provides defense-in-depth\nfor stored objects.\n\nThe rule is structured as a chain of equality checks ending in an explicit\n`false`, so adding a new UpstreamProviderType value without extending this\nrule fails admission instead of silently demanding the OAuth2 shape. When\nadding a new type, extend both this rule and validateUpstreamProvider.", "properties": { "name": { "description": "Name uniquely identifies this upstream provider.\nUsed for routing decisions and session binding in multi-upstream scenarios.\nMust be lowercase alphanumeric with hyphens (DNS-label-like).", @@ -320,7 +336,7 @@ "type": "string" }, "clientId": { - "description": "ClientID is the OAuth 2.0 client identifier registered with the upstream IDP.", + "description": "ClientID is the OAuth 2.0 client identifier registered with the upstream IDP.\nMutually exclusive with DCRConfig: when DCRConfig is set, ClientID is obtained\nat runtime via RFC 7591 Dynamic Client Registration and must be left empty.", "type": "string" }, "clientSecretRef": { @@ -341,6 +357,58 @@ ], "type": "object" }, + "dcrConfig": { + "description": "DCRConfig enables RFC 7591 Dynamic Client Registration against the upstream\nauthorization server. When set, the client credentials are obtained at\nruntime rather than being pre-provisioned, and ClientID must be left empty.\nMutually exclusive with ClientID.", + "properties": { + "discoveryUrl": { + "description": "DiscoveryURL is the RFC 8414 / OIDC Discovery document URL. The resolver\nissues a single GET against this URL (no well-known-path fallback) and\nreads registration_endpoint, authorization_endpoint, token_endpoint,\ntoken_endpoint_auth_methods_supported, and scopes_supported from the\nresponse.\nMutually exclusive with RegistrationEndpoint.\nHTTPS is required because the registration endpoint resolved from this\ndocument carries the initial access token and the issued client_secret\n(RFC 7591 §3, RFC 8414 §3). MaxLength is a defensive size cap (etcd\nobject budget, regex evaluation cost) and matches the conventional URL\nlength cap.", + "maxLength": 2048, + "pattern": "^https://[^\\s?#]+[^/\\s?#]$", + "type": "string" + }, + "initialAccessTokenRef": { + "description": "InitialAccessTokenRef is an optional reference to a Kubernetes Secret\ncarrying an RFC 7591 §3 initial access token. When set, the resolver\npresents the token value as a Bearer credential on the registration\nrequest. Mirrors the ClientSecretRef pattern.", + "properties": { + "key": { + "description": "Key is the key within the secret", + "type": "string" + }, + "name": { + "description": "Name is the name of the secret", + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "registrationEndpoint": { + "description": "RegistrationEndpoint is the RFC 7591 registration endpoint URL used\ndirectly, bypassing discovery. When using this field, the caller is\nexpected to also supply AuthorizationEndpoint, TokenEndpoint, and an\nexplicit Scopes list on the parent OAuth2UpstreamConfig.\nMutually exclusive with DiscoveryURL.\nHTTPS is required because the registration endpoint carries the initial\naccess token and the issued client_secret (RFC 7591 §3, RFC 8414 §3).\nMaxLength is a defensive size cap (etcd object budget, regex evaluation\ncost) and matches the conventional URL length cap.", + "maxLength": 2048, + "pattern": "^https://[^\\s?#]+[^/\\s?#]$", + "type": "string" + }, + "softwareId": { + "description": "SoftwareID is the RFC 7591 \"software_id\" registration metadata value,\nidentifying the client software independent of any particular\nregistration instance. Typically a UUID or short identifier.", + "maxLength": 255, + "type": "string" + }, + "softwareStatement": { + "description": "SoftwareStatement is the RFC 7591 \"software_statement\" JWT asserting\nmetadata about the client software, signed by a party the authorization\nserver trusts.\n\nStored inline on the CR. The JWT is signed but not encrypted, so its\ncontents are visible to anyone with get/list/watch on this resource and\nappear in etcd backups in plaintext. Treat the value as non-confidential\n(signed attestation, not a secret). Operators that rotate software\nstatements like bearer credentials should keep them at the authorization\nserver side and rely on the registration endpoint's initial access\ntoken (see InitialAccessTokenRef) instead of placing them on the CR.\n\nBounded to 16384 characters as a defensive size cap (etcd object\nbudget, regex evaluation cost). Real-world signed statements with\nembedded x5c certificate chains, JWKS keys, or OIDC-Federation\ntrust-framework metadata routinely exceed 4 KB.", + "maxLength": 16384, + "type": "string" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of discoveryUrl or registrationEndpoint must be set", + "rule": "has(self.discoveryUrl) != has(self.registrationEndpoint)" + } + ] + }, "redirectUri": { "description": "RedirectURI is the callback URL where the upstream IDP will redirect after authentication.\nWhen not specified, defaults to `{resourceUrl}/oauth/callback` where `resourceUrl` is the\nURL associated with the resource (e.g., MCPServer or vMCP) using this config.", "type": "string" @@ -446,10 +514,19 @@ }, "required": [ "authorizationEndpoint", - "clientId", "tokenEndpoint" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "exactly one of clientId or dcrConfig must be set", + "rule": "(has(self.clientId) && size(self.clientId) > 0) ? !has(self.dcrConfig) : has(self.dcrConfig)" + }, + { + "message": "clientSecretRef must not be set when dcrConfig is set; the client_secret is obtained at runtime via Dynamic Client Registration", + "rule": "!(has(self.dcrConfig) && has(self.clientSecretRef))" + } + ] }, "oidcConfig": { "description": "OIDCConfig contains OIDC-specific configuration.\nRequired when Type is \"oidc\", must be nil when Type is \"oauth2\".", @@ -580,7 +657,13 @@ "name", "type" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "type must be 'oidc' or 'oauth2'; oidcConfig must be set when type is 'oidc' and oauth2Config must be set when type is 'oauth2' (and the other must not be set)", + "rule": "self.type == 'oidc' ? (has(self.oidcConfig) && !has(self.oauth2Config)) : self.type == 'oauth2' ? (has(self.oauth2Config) && !has(self.oidcConfig)) : false" + } + ] }, "minItems": 1, "type": "array", @@ -1847,6 +1930,13 @@ "minItems": 1, "type": "array", "x-kubernetes-list-type": "atomic" + }, + "primaryUpstreamProvider": { + "description": "PrimaryUpstreamProvider names the upstream IDP whose access token's claims\nCedar should evaluate. Currently honored only when the parent\nAuthzConfigRef.Type is \"inline\"; configMap-sourced policies will support\nthis in a future release (see #5208). Only meaningful for VirtualMCPServer\nwith an embedded auth server. When empty and an embedded auth server has\nupstreams configured, the controller defaults to the first upstream\nprovider. The name must match one of the upstreams declared on\nspec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is\nrejected with AuthServerConfigValidated=False. MCPServer and MCPRemoteProxy\nhave no embedded auth server; setting this field on those CRs surfaces an\nAuthzPrimaryUpstreamProviderIgnored advisory condition on the resource.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" } }, "required": [ diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 6d973f5c..46fa4b74 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -280,6 +280,14 @@ components: type: string bearer_token_file: type: string + cached_cimd_client_id: + description: |- + CachedCIMDClientID stores the CIMD metadata URL used as client_id when CIMD + authentication was used. Kept separate from CachedClientID (which holds + DCR-issued IDs) so the two can have independent lifecycles — DCR credential + rotation clears CachedClientID without touching the stable CIMD URL. + Read by resolveClientCredentials to send the correct client_id on token refresh. + type: string cached_client_id: description: |- Cached DCR client credentials for persistence across restarts. @@ -415,9 +423,21 @@ components: properties: discovery_url: description: |- - DiscoveryURL is the RFC 8414 / OIDC Discovery URL from which the - registration_endpoint is resolved at runtime. Mutually exclusive with - RegistrationEndpoint. + DiscoveryURL is the exact RFC 8414 / OIDC Discovery document URL to + fetch at runtime. The resolver issues a single GET against this URL + (no well-known-path fallback) and reads registration_endpoint, + authorization_endpoint, token_endpoint, + token_endpoint_auth_methods_supported, and scopes_supported from the + response. Per RFC 8414 §3.3, the document's "issuer" field must + exactly match the upstream issuer configured on the parent + run-config. + + Use this field when the upstream publishes discovery metadata at a + path that differs from the issuer-derived well-known paths — for + example a multi-tenant IdP whose metadata lives at + https://idp.example.com/tenants/acme/.well-known/openid-configuration. + + Mutually exclusive with RegistrationEndpoint. type: string initial_access_token_env_var: description: |- @@ -435,7 +455,14 @@ components: registration_endpoint: description: |- RegistrationEndpoint is the RFC 7591 registration endpoint URL used - directly, bypassing discovery. Mutually exclusive with DiscoveryURL. + directly, bypassing discovery. Because no discovery is performed, + server-capability fields (token_endpoint_auth_methods_supported, + scopes_supported) are unavailable on this code path; the caller is + expected to also supply AuthorizationEndpoint, TokenEndpoint, and an + explicit Scopes list on the parent OAuth2UpstreamRunConfig. Auth + method falls back to the resolver's default (client_secret_basic). + + Mutually exclusive with DiscoveryURL. type: string software_id: description: |- @@ -791,10 +818,19 @@ components: properties: acl_user_config: $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver_storage.ACLUserRunConfig' + addr: + description: |- + Addr is the Redis server address (host:port). Required for standalone and cluster modes. + Mutually exclusive with SentinelConfig. + type: string auth_type: description: AuthType must be "aclUser" - only ACL user authentication is supported. type: string + cluster_mode: + description: ClusterMode enables the Redis Cluster protocol. Requires Addr + to be set. + type: boolean dial_timeout: description: DialTimeout is the timeout for establishing connections (e.g., "5s"). @@ -816,9 +852,8 @@ components: type: string type: object github_com_stacklok_toolhive_pkg_authserver_storage.RedisTLSRunConfig: - description: |- - SentinelTLS configures TLS for Sentinel connections. - Falls back to TLS config when nil. + description: SentinelTLS configures TLS for Sentinel connections. Only applies + when SentinelConfig is set. properties: ca_cert_file: description: CACertFile is the path to a PEM-encoded CA certificate file. @@ -839,7 +874,9 @@ components: type: string type: object github_com_stacklok_toolhive_pkg_authserver_storage.SentinelRunConfig: - description: SentinelConfig contains Sentinel-specific configuration. + description: |- + SentinelConfig contains Sentinel-specific configuration. + Mutually exclusive with Addr. properties: db: description: 'DB is the Redis database number (default: 0).'