From 5b6549b3775a07444f9c704552bfbd5c63472bf4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:38:34 +0000 Subject: [PATCH 1/3] Update stacklok/toolhive to v0.48.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/upstream-projects.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index e646dc3c..f7c001e5 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -44,7 +44,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.47.1 + version: v0.48.0 # toolhive is a monorepo covering the CLI, the Kubernetes # operator, and the vMCP gateway. It also introduces cross- # cutting features that land in concepts/, integrations/, From ea0b193499111331a8e7a35b1427927c8a3525a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 11 Sep 2026 09:39:40 +0000 Subject: [PATCH 2/3] Refresh reference assets for toolhive v0.48.0 --- docs/toolhive/reference/cli/thv_ai-plugin.md | 5 +- .../reference/cli/thv_ai-plugin_install.md | 8 +- .../reference/cli/thv_ai-plugin_push.md | 3 + .../reference/cli/thv_ai-plugin_sync.md | 2 +- .../reference/cli/thv_ai-plugin_upgrade.md | 2 +- docs/toolhive/reference/cli/thv_run.md | 1 + docs/toolhive/reference/cli/thv_skill_push.md | 2 +- static/api-specs/toolhive-api.yaml | 51 ++++++++++- .../mcpexternalauthconfigs.schema.json | 86 ++++++++++++++++++- .../toolhive-crds/mcpservers.schema.json | 11 +++ .../virtualmcpservers.schema.json | 86 ++++++++++++++++++- 11 files changed, 240 insertions(+), 17 deletions(-) diff --git a/docs/toolhive/reference/cli/thv_ai-plugin.md b/docs/toolhive/reference/cli/thv_ai-plugin.md index c2bc6ec5..67368956 100644 --- a/docs/toolhive/reference/cli/thv_ai-plugin.md +++ b/docs/toolhive/reference/cli/thv_ai-plugin.md @@ -15,8 +15,9 @@ Manage AI-tool plugins ### Synopsis -The ai-plugin command provides subcommands to manage plugins for AI tools -(e.g. Claude Code, Codex) — not plugins for ToolHive itself. +Manage plugins for AI tools such as Claude Code and Codex, not plugins +for ToolHive itself. A plugin is a manifest-based bundle that may contain +commands, agents, skills, hooks, and server declarations. ### Options diff --git a/docs/toolhive/reference/cli/thv_ai-plugin_install.md b/docs/toolhive/reference/cli/thv_ai-plugin_install.md index fd2f770d..fd886d34 100644 --- a/docs/toolhive/reference/cli/thv_ai-plugin_install.md +++ b/docs/toolhive/reference/cli/thv_ai-plugin_install.md @@ -15,8 +15,12 @@ Install an AI-tool plugin ### Synopsis -Install a plugin by name or OCI reference. -The plugin will be fetched from a remote registry and installed locally. +Install a plugin from git, an OCI reference, or an exact registry name. + +Project-scoped installs verify signatures and record trust in toolhive.lock.yaml. +Use --public-key for the first project install of a key-pair-signed OCI artifact; +the key is then pinned for sync and upgrade. User-scoped installs do not use +lock-file verification and reject --public-key. ``` thv ai-plugin install [plugin-name] [flags] diff --git a/docs/toolhive/reference/cli/thv_ai-plugin_push.md b/docs/toolhive/reference/cli/thv_ai-plugin_push.md index ed832ec8..b4081b83 100644 --- a/docs/toolhive/reference/cli/thv_ai-plugin_push.md +++ b/docs/toolhive/reference/cli/thv_ai-plugin_push.md @@ -17,6 +17,9 @@ Push a built AI-tool plugin to an OCI registry Push a previously built plugin artifact to a remote OCI registry. +Push signs keylessly by default. Use --no-sign to publish unsigned; plugin push +does not support key-pair signing and has no --key flag. + ``` thv ai-plugin push [reference] [flags] ``` diff --git a/docs/toolhive/reference/cli/thv_ai-plugin_sync.md b/docs/toolhive/reference/cli/thv_ai-plugin_sync.md index a7d2a08b..1f38c6ea 100644 --- a/docs/toolhive/reference/cli/thv_ai-plugin_sync.md +++ b/docs/toolhive/reference/cli/thv_ai-plugin_sync.md @@ -36,7 +36,7 @@ thv ai-plugin sync [flags] --adopt Write lock entries for existing unmanaged project-scope installs --allow-unsigned Record plugins as unsigned in the lock file: when adopting installs whose signature state cannot be established (--adopt), and when repairing an entry that records no trust decision and whose content is unsigned --check Report drift without installing, writing, or removing anything - --clients string Comma-separated target client apps (e.g. claude-code,opencode), or "all" for every available client + --clients string Comma-separated target client apps (e.g. claude-code,codex), or "all" for every available client --format string Output format (json, text) (default "text") -h, --help help for sync --project-root string Project root path (default: auto-detected from the current directory) diff --git a/docs/toolhive/reference/cli/thv_ai-plugin_upgrade.md b/docs/toolhive/reference/cli/thv_ai-plugin_upgrade.md index 8027179e..27bc6105 100644 --- a/docs/toolhive/reference/cli/thv_ai-plugin_upgrade.md +++ b/docs/toolhive/reference/cli/thv_ai-plugin_upgrade.md @@ -40,7 +40,7 @@ thv ai-plugin upgrade [plugin-name...] [flags] ``` --allow-ref-change Permit the artifact to move to a different repository during upgrade --allow-signer-change Permit upgrading to an artifact signed by a different identity; the new identity replaces the recorded one - --clients string Comma-separated target client apps (e.g. claude-code,opencode), or "all" for every available client + --clients string Comma-separated target client apps (e.g. claude-code,codex), or "all" for every available client --fail-on-changes Report what would change without installing anything; a CI freshness gate --format string Output format (json, text) (default "text") -h, --help help for upgrade diff --git a/docs/toolhive/reference/cli/thv_run.md b/docs/toolhive/reference/cli/thv_run.md index 87fa8800..17b6c461 100644 --- a/docs/toolhive/reference/cli/thv_run.md +++ b/docs/toolhive/reference/cli/thv_run.md @@ -160,6 +160,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] --print-resolved-overlays Debug: show resolved container paths for tmpfs overlays (default false) --proxy-mode string Proxy mode for stdio (streamable-http or sse (deprecated, will be removed)) (default "streamable-http") --proxy-port int Port for the HTTP proxy to listen on (host port) + --proxy-read-timeout duration Maximum time to read a full request on the proxy (e.g., 30s, 1m); zero uses the default (30s) -p, --publish stringArray Publish a container's port(s) to the host (format: hostPort:containerPort) --remote-auth Enable OAuth/OIDC authentication to remote MCP server (default false) --remote-auth-authorize-url string OAuth authorization endpoint URL (alternative to --remote-auth-issuer for non-OIDC OAuth) diff --git a/docs/toolhive/reference/cli/thv_skill_push.md b/docs/toolhive/reference/cli/thv_skill_push.md index 39779c6f..c9a41898 100644 --- a/docs/toolhive/reference/cli/thv_skill_push.md +++ b/docs/toolhive/reference/cli/thv_skill_push.md @@ -26,7 +26,7 @@ thv skill push [reference] [flags] ``` -h, --help help for push --identity-token string OIDC identity token (or a path to a file containing one) for keyless signing. Mutually exclusive with --key. If omitted, one is acquired automatically: from the ambient CI OIDC token when running with id-token: write permission, otherwise via an interactive browser sign-in - --key string Path to a cosign private key to sign the pushed artifact. Encrypted keys are decrypted with COSIGN_PASSWORD read from the 'thv serve' process, which performs the signing. Consumers installing the result project-scoped must pass --public-key with the matching cosign public key the first time; distribute it alongside the artifact. Keyless signing needs no such out-of-band step, since the signer identity is verifiable from the artifact itself + --key string Path to a cosign private key to sign the pushed artifact. Requires the locally discovered ToolHive server; for a remote or manually configured API URL, use keyless signing. Encrypted keys are decrypted with COSIGN_PASSWORD read from the 'thv serve' process, which performs the signing. Consumers installing the result project-scoped must pass --public-key with the matching cosign public key the first time; distribute it alongside the artifact. Keyless signing needs no such out-of-band step, since the signer identity is verifiable from the artifact itself --no-sign Push without signing (consumers will need an explicit unsigned exception to install project-scoped) ``` diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 7ad50b82..afd6c1d7 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -392,6 +392,14 @@ components: token_endpoint: description: TokenEndpoint is the URL for the OAuth token endpoint. type: string + token_endpoint_auth_method: + description: |- + TokenEndpointAuthMethod selects how the client authenticates at the OAuth token + endpoint. When empty and a client secret is configured, client_secret_basic is + used, matching the RFC 7591 default for confidential clients. Set this to + client_secret_post only for providers that require credentials in the request body. + Public clients without a secret use the "none" method. + type: string token_response_mapping: $ref: '#/components/schemas/authserver.TokenResponseMappingRunConfig' userinfo: @@ -436,6 +444,8 @@ components: ClientSecretFile is the path to a file containing the OAuth 2.0 client secret. Mutually exclusive with ClientSecretEnvVar. Optional for public clients using PKCE. type: string + dcr_config: + $ref: '#/components/schemas/authserver.DCRUpstreamConfig' insecure_allow_http: description: |- InsecureAllowHTTP permits a plain-HTTP issuer URL and HTTP discovery @@ -1889,6 +1899,15 @@ components: - sse - streamable-http type: string + proxy_read_timeout: + description: |- + ProxyReadTimeout bounds reading the entire request (headers + body) on the + proxy HTTP server, expressed as a Go duration string (e.g. "30s", "1m"). + Empty uses the proxy default (30s). Negative durations and values that fail + time.ParseDuration are rejected at runtime. Applies to all HTTP transports. + String (not time.Duration) keeps the wire format unit-explicit. + example: 30s + type: string publish: description: Publish lists ports to publish to the host in format "hostPort:containerPort" items: @@ -3154,6 +3173,12 @@ components: proxy_port: description: Port for the HTTP proxy to listen on type: integer + proxy_read_timeout: + description: |- + Maximum time to read a complete MCP proxy request, expressed as a Go duration string. + Empty or zero uses the default timeout of 30 seconds. + example: 30s + type: string registry: description: Registry is the optional registry name to resolve the server from (e.g. "default"). @@ -3578,8 +3603,12 @@ components: type: string key: description: |- - Key is the path to a cosign private key used to sign the pushed - artifact + Key is the path to a cosign private key, resolved on the server's + filesystem. Accepted only when the request carries the secret capability + from the owner-protected local server discovery file; other requests are + refused with 403, since honoring one would let an untrusted caller have + the server sign with any key it can read. Use IdentityToken when calling + a remote or manually configured server. type: string no_sign: description: NoSign pushes without signing @@ -3902,6 +3931,12 @@ components: proxy_port: description: Port for the HTTP proxy to listen on type: integer + proxy_read_timeout: + description: |- + Maximum time to read a complete MCP proxy request, expressed as a Go duration string. + Empty or zero uses the default timeout of 30 seconds. + example: 30s + type: string runtime_config: $ref: '#/components/schemas/templates.RuntimeConfig' secrets: @@ -7251,6 +7286,12 @@ paths: /api/v1beta/skills/push: post: description: Push a built skill artifact to a remote registry + parameters: + - description: Local discovery capability (required with request.key) + in: header + name: X-Toolhive-Key-Signing-Capability + schema: + type: string requestBody: content: application/json: @@ -7275,6 +7316,12 @@ paths: schema: type: string description: Bad Request + "403": + content: + application/json: + schema: + type: string + description: Forbidden (key signing requires the local discovery capability) "404": content: application/json: diff --git a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json index 5ac406db..b95fa920 100644 --- a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json @@ -1368,8 +1368,8 @@ "type": "object", "x-kubernetes-validations": [ { - "message": "exactly one of discoveryUrl or registrationEndpoint must be set", - "rule": "has(self.discoveryUrl) != has(self.registrationEndpoint)" + "message": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.discoveryUrl) && has(self.registrationEndpoint))" } ] }, @@ -1421,6 +1421,15 @@ "pattern": "^https?://.*$", "type": "string" }, + "tokenEndpointAuthMethod": { + "description": "TokenEndpointAuthMethod selects how the client authenticates at the OAuth token\nendpoint. When empty and a client secret is configured, client_secret_basic is\nused, matching the RFC 7591 default for confidential clients. Set this to\nclient_secret_post only for providers that require credentials in the request body.\nPublic clients without a secret use the \"none\" method.", + "enum": [ + "none", + "client_secret_basic", + "client_secret_post" + ], + "type": "string" + }, "tokenResponseMapping": { "description": "TokenResponseMapping configures custom field extraction from non-standard token responses.\nSome OAuth providers (e.g., GovSlack) nest token fields under non-standard paths\ninstead of returning them at the top level. When set, ToolHive performs the token\nexchange HTTP call directly and extracts fields using the configured dot-notation paths.\nIf nil, standard OAuth 2.0 token response parsing is used.\nFor extracting user identity from the token response, see IdentityFromToken.", "properties": { @@ -1517,6 +1526,10 @@ "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": "exactly one of discoveryUrl or registrationEndpoint must be set when dcrConfig is set", + "rule": "!has(self.dcrConfig) || (has(self.dcrConfig.discoveryUrl) != has(self.dcrConfig.registrationEndpoint))" + }, { "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))" @@ -1595,6 +1608,58 @@ ], "type": "object" }, + "dcrConfig": { + "description": "DCRConfig enables RFC 7591 Dynamic Client Registration. When set, ClientID\nand ClientSecretRef must be omitted. If neither discoveryUrl nor\nregistrationEndpoint is set, discovery is derived from issuerUrl.", + "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": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.discoveryUrl) && has(self.registrationEndpoint))" + } + ] + }, "issuerUrl": { "description": "IssuerURL is the OIDC issuer URL for automatic endpoint discovery.\nMust be a valid HTTPS URL.", "pattern": "^https://.*$", @@ -1679,10 +1744,23 @@ } }, "required": [ - "clientId", "issuerUrl" ], - "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))" + }, + { + "message": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.dcrConfig) && has(self.dcrConfig.discoveryUrl) && has(self.dcrConfig.registrationEndpoint))" + } + ] }, "type": { "description": "Type specifies the provider type: \"oidc\" or \"oauth2\"", diff --git a/static/api-specs/toolhive-crds/mcpservers.schema.json b/static/api-specs/toolhive-crds/mcpservers.schema.json index 3b1667df..ee1e0d72 100644 --- a/static/api-specs/toolhive-crds/mcpservers.schema.json +++ b/static/api-specs/toolhive-crds/mcpservers.schema.json @@ -324,6 +324,17 @@ "minimum": 1, "type": "integer" }, + "proxyReadTimeout": { + "description": "ProxyReadTimeout bounds how long the proxy spends reading a full request\n(headers + body), mitigating slow-upload connection exhaustion. Applies to\nall transports. Defaults to 30s if not specified. Example: \"1m\".", + "format": "duration", + "type": "string", + "x-kubernetes-validations": [ + { + "message": "proxyReadTimeout must be non-negative", + "rule": "duration(self) >= duration('0s')" + } + ] + }, "rateLimiting": { "description": "RateLimiting defines rate limiting configuration for the MCP server.\nRequires Redis session storage to be configured for distributed rate limiting.", "properties": { diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json index 5a9e1c53..3454571c 100644 --- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json +++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json @@ -1259,8 +1259,8 @@ "type": "object", "x-kubernetes-validations": [ { - "message": "exactly one of discoveryUrl or registrationEndpoint must be set", - "rule": "has(self.discoveryUrl) != has(self.registrationEndpoint)" + "message": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.discoveryUrl) && has(self.registrationEndpoint))" } ] }, @@ -1312,6 +1312,15 @@ "pattern": "^https?://.*$", "type": "string" }, + "tokenEndpointAuthMethod": { + "description": "TokenEndpointAuthMethod selects how the client authenticates at the OAuth token\nendpoint. When empty and a client secret is configured, client_secret_basic is\nused, matching the RFC 7591 default for confidential clients. Set this to\nclient_secret_post only for providers that require credentials in the request body.\nPublic clients without a secret use the \"none\" method.", + "enum": [ + "none", + "client_secret_basic", + "client_secret_post" + ], + "type": "string" + }, "tokenResponseMapping": { "description": "TokenResponseMapping configures custom field extraction from non-standard token responses.\nSome OAuth providers (e.g., GovSlack) nest token fields under non-standard paths\ninstead of returning them at the top level. When set, ToolHive performs the token\nexchange HTTP call directly and extracts fields using the configured dot-notation paths.\nIf nil, standard OAuth 2.0 token response parsing is used.\nFor extracting user identity from the token response, see IdentityFromToken.", "properties": { @@ -1408,6 +1417,10 @@ "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": "exactly one of discoveryUrl or registrationEndpoint must be set when dcrConfig is set", + "rule": "!has(self.dcrConfig) || (has(self.dcrConfig.discoveryUrl) != has(self.dcrConfig.registrationEndpoint))" + }, { "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))" @@ -1486,6 +1499,58 @@ ], "type": "object" }, + "dcrConfig": { + "description": "DCRConfig enables RFC 7591 Dynamic Client Registration. When set, ClientID\nand ClientSecretRef must be omitted. If neither discoveryUrl nor\nregistrationEndpoint is set, discovery is derived from issuerUrl.", + "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": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.discoveryUrl) && has(self.registrationEndpoint))" + } + ] + }, "issuerUrl": { "description": "IssuerURL is the OIDC issuer URL for automatic endpoint discovery.\nMust be a valid HTTPS URL.", "pattern": "^https://.*$", @@ -1570,10 +1635,23 @@ } }, "required": [ - "clientId", "issuerUrl" ], - "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))" + }, + { + "message": "at most one of discoveryUrl or registrationEndpoint may be set", + "rule": "!(has(self.dcrConfig) && has(self.dcrConfig.discoveryUrl) && has(self.dcrConfig.registrationEndpoint))" + } + ] }, "type": { "description": "Type specifies the provider type: \"oidc\" or \"oauth2\"", From 05bbcc2d02ef68356bd06baf31bc67941b3603ee Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:55:28 +0000 Subject: [PATCH 3/3] Polish v0.48.0 upstream release doc edits - Fix contradiction in DCR guidance: "at most one" instead of "exactly one", matching the OIDC case where empty dcrConfig is valid - Remove duplicated "Don't set clientId" note in the OAuth 2.0 DCR subsection (already covered in the OIDC subsection above) - Polish proxy read timeout prose: fix sentence fragment, rephrase the "does not affect SSE" negation positively, drop hedging - Change "drop --key" to "omit --key" in the skill push signing note --- .../guides-cli/configure-mcp-servers.mdx | 19 +++++ .../toolhive/guides-cli/skills-management.mdx | 4 + .../guides-k8s/embedded-auth-server-k8s.mdx | 84 ++++++++++++++++--- .../guides-vmcp/embedded-auth-server-vmcp.mdx | 50 ++++++++++- 4 files changed, 143 insertions(+), 14 deletions(-) diff --git a/docs/toolhive/guides-cli/configure-mcp-servers.mdx b/docs/toolhive/guides-cli/configure-mcp-servers.mdx index efa33907..1aefb10a 100644 --- a/docs/toolhive/guides-cli/configure-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/configure-mcp-servers.mdx @@ -97,6 +97,25 @@ thv run --session-ttl 4h Set a longer value when clients hold sessions open for long-running operations, or a shorter value to free resources faster. +### Adjust the proxy read timeout + +The proxy bounds how long it spends reading a full incoming request (headers +plus body). This limit protects the proxy from slow uploads that hold +connections open; long-lived SSE responses stream normally because the limit +applies to request bodies only. + +The default is 30 seconds. Raise it for workloads that upload large payloads, or +lower it to fail slow uploads faster: + +```bash +thv run --proxy-read-timeout 2m +``` + +The flag accepts any Go duration string, for example `45s` or `2m`. Omitting the +flag or passing `0` keeps the 30-second default; negative values are rejected. +The setting applies to the streamable HTTP, SSE, and transparent proxy +transports. + ### Run a server in the foreground By default, ToolHive runs the server in the background and returns control to diff --git a/docs/toolhive/guides-cli/skills-management.mdx b/docs/toolhive/guides-cli/skills-management.mdx index 88938f19..34fb5074 100644 --- a/docs/toolhive/guides-cli/skills-management.mdx +++ b/docs/toolhive/guides-cli/skills-management.mdx @@ -405,6 +405,10 @@ thv skill push ghcr.io/my-org/skills/my-skill:v1.0.0 error before publishing anything. - **Key-pair with `--key`**: sign with a cosign private key on disk. Set `COSIGN_PASSWORD` in the `thv serve` environment to decrypt an encrypted key. + Requires the locally discovered ToolHive server; a push to a remote or + manually configured server (for example, via `TOOLHIVE_API_URL`) returns + `403`. Use `--identity-token` or omit `--key` for keyless signing against + those servers. - **Explicitly unsigned with `--no-sign`**: publish without any signature. The three signing inputs are mutually exclusive. Combining `--key`, diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index e943e1a9..b0c711c2 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -676,8 +676,8 @@ this section to adapt that upstream connection: - Use `oauth2Config` instead of `oidcConfig` when the upstream provider does not support OIDC discovery. -- Within `oauth2Config`, set either pre-provisioned client credentials or - `dcrConfig`, not both. +- Within `oidcConfig` or `oauth2Config`, set either pre-provisioned client + credentials or `dcrConfig`, not both. - Add identity mapping, authorization parameters, and an explicit callback URL as the upstream provider requires. @@ -759,6 +759,42 @@ For OAuth 2.0 servers that return identity in the token response itself, see ::: +#### Select the token endpoint auth method + +When a client secret is configured, the embedded auth server sends the client +credentials to the upstream token endpoint using HTTP Basic auth +(`client_secret_basic`). This matches the RFC 7591 default for confidential +clients and covers most providers. Public clients without a secret use the +`none` method. + +Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for +providers that require credentials in the request body instead of the +`Authorization` header: + +```yaml title="MCPExternalAuthConfig: token endpoint auth method" +oauth2Config: + authorizationEndpoint: 'https://example.com/oauth/authorize' + tokenEndpoint: 'https://example.com/oauth/token' + clientId: '' + clientSecretRef: + name: upstream-idp-secret + key: client-secret + # highlight-next-line + tokenEndpointAuthMethod: client_secret_post +``` + +Allowed values are `none`, `client_secret_basic`, and `client_secret_post`. DCR +clients continue to use the method negotiated at registration time. + +:::info[Changed in v0.48.0] + +Pre-registered OAuth 2.0 upstream clients with a configured secret previously +sent credentials in the request body. They now default to `client_secret_basic`. +If your provider rejects Basic auth on the token endpoint (rare), set +`tokenEndpointAuthMethod: client_secret_post` to restore the previous behavior. + +::: + ### Trust a private CA for the upstream provider If the upstream identity provider serves its endpoints with a certificate signed @@ -817,8 +853,8 @@ the two references configure trust for different network hops. ### Use dynamic client registration with an upstream provider -Some OAuth 2.0 providers register clients dynamically instead of requiring you -to create an application in a provider dashboard. Add `dcrConfig` to an +Some providers register clients dynamically instead of requiring you to create +an application in a provider dashboard. Add `dcrConfig` to an `oidcConfig` or `oauth2Config` upstream to have the embedded authorization server register itself at runtime using RFC 7591. @@ -827,8 +863,39 @@ This is separate from MCP clients registering with ToolHive's embedded authorization server in [Configure MCP client registration](#configure-mcp-client-registration). -This example uses an RFC 8414 discovery document. ToolHive reads the -`registration_endpoint` and other provider metadata from `discoveryUrl`: +#### DCR with an OIDC upstream + +For an OIDC upstream, an empty `dcrConfig: {}` is enough: the embedded auth +server derives the discovery URL from +`issuerUrl + /.well-known/openid-configuration` and reads the registration +endpoint from the document. + +```yaml title="MCPExternalAuthConfig: OIDC upstream with DCR" +spec: + type: embeddedAuthServer + embeddedAuthServer: + issuer: 'https://toolhive.example.com' + upstreamProviders: + - name: keycloak + type: oidc + oidcConfig: + issuerUrl: 'https://keycloak.example.com/realms/main' + # highlight-start + dcrConfig: {} + # highlight-end +``` + +Set `dcrConfig.discoveryUrl` explicitly when the upstream publishes discovery at +a non-standard path, or `dcrConfig.registrationEndpoint` to bypass discovery +entirely. + +Don't set `clientId` or `clientSecretRef` when you set `dcrConfig`. ToolHive +obtains the client ID and client secret from the DCR response. + +#### DCR with an OAuth 2.0 upstream + +For an OAuth 2.0 upstream, use an RFC 8414 discovery document. ToolHive reads +the `registration_endpoint` and other provider metadata from `discoveryUrl`: ```yaml title="embedded-auth-config-dcr.yaml" apiVersion: toolhive.stacklok.dev/v1beta1 @@ -884,13 +951,10 @@ upstreamProviders: # highlight-end ``` -Set exactly one of `discoveryUrl` or `registrationEndpoint`. A direct +Set at most one of `discoveryUrl` or `registrationEndpoint`. A direct registration endpoint bypasses discovery, so specify the authorization and token endpoints and the scopes that ToolHive should request. -Don't set `clientId` or `clientSecretRef` when you set `dcrConfig`. ToolHive -obtains the client ID and client secret from the DCR response. - #### Authorize registration with an initial access token If the upstream provider requires an initial access token, create a Secret in diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index 52d4cc1a..be045fff 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -406,10 +406,30 @@ for guidance on which scopes to include. This section configures ToolHive's OAuth connection to an external provider. It is separate from incoming registration between MCP clients and ToolHive. Use a preconfigured client ID and secret, or add `dcrConfig` to register ToolHive at -runtime when the provider supports RFC 7591. +runtime when the provider supports RFC 7591. DCR works with both `oidcConfig` +and `oauth2Config` upstreams. -This example uses an RFC 8414 discovery document to locate the provider's -`registration_endpoint`: +For an OIDC upstream, an empty `dcrConfig: {}` is enough: the embedded auth +server derives the discovery URL from +`issuerUrl + /.well-known/openid-configuration` and reads the registration +endpoint from the document. + +```yaml title="VirtualMCPServer: OIDC upstream with DCR" +spec: + authServerConfig: + issuer: 'https://toolhive.example.com' + upstreamProviders: + - name: keycloak + type: oidc + oidcConfig: + issuerUrl: 'https://keycloak.example.com/realms/main' + # highlight-start + dcrConfig: {} + # highlight-end +``` + +For an OAuth 2.0 upstream, use an RFC 8414 discovery document to locate the +provider's `registration_endpoint`: ```yaml title="VirtualMCPServer: upstream DCR discovery" spec: @@ -439,7 +459,7 @@ dcrConfig: registrationEndpoint: 'https://mcp.example.com/register' ``` -Set exactly one of `discoveryUrl` or `registrationEndpoint`. A direct +Set at most one of `discoveryUrl` or `registrationEndpoint`. A direct registration endpoint bypasses discovery, so keep the authorization endpoint, token endpoint, and scopes in the parent `oauth2Config`. With `dcrConfig`, ToolHive obtains the client ID and secret from the DCR response. @@ -450,6 +470,28 @@ ToolHive obtains the client ID and secret from the DCR response. | `dcrConfig.registrationEndpoint` | HTTPS URL of the RFC 7591 registration endpoint. | | `dcrConfig.initialAccessTokenRef` | Optional reference to a Secret whose value ToolHive sends as a bearer token when it calls the registration endpoint. Requires `name` and `key`. | +### Select the token endpoint auth method + +When a client secret is configured, ToolHive sends the credentials to the +upstream OAuth 2.0 token endpoint using HTTP Basic auth (`client_secret_basic`). +This matches the RFC 7591 default for confidential clients and covers most +providers. Public clients without a secret use the `none` method. + +Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for +providers that require credentials in the request body instead of the +`Authorization` header. Allowed values are `none`, `client_secret_basic`, and +`client_secret_post`. DCR clients continue to use the method negotiated at +registration time. + +:::info[Changed in v0.48.0] + +Pre-registered OAuth 2.0 upstream clients with a configured secret previously +sent credentials in the request body. They now default to `client_secret_basic`. +If your provider rejects Basic auth on the token endpoint (rare), set +`tokenEndpointAuthMethod: client_secret_post` to restore the previous behavior. + +::: + ### Authorize upstream registration with an initial access token If the upstream provider requires an initial access token, create a Secret in