From 91e306f8fb0f8dcfab742be37e872199a685c418 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:33:12 +0000 Subject: [PATCH 1/6] Update stacklok/toolhive to v0.28.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 1451198e..1ec6b8ba 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -35,7 +35,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.27.2 + version: v0.28.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 6ce606212debc4db244ce44abd2ffcd923fa9d6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 May 2026 18:34:06 +0000 Subject: [PATCH 2/6] Refresh reference assets for toolhive v0.28.0 --- .../reference/cli/thv_client_register.md | 1 + .../reference/cli/thv_client_remove.md | 1 + docs/toolhive/reference/cli/thv_run.md | 1 + docs/toolhive/reference/cli/thv_vmcp_serve.md | 1 + .../crds/mcpexternalauthconfigs.schema.json | 36 +++- .../api-specs/crds/mcpoidcconfigs.schema.json | 5 + .../api-specs/crds/mcptoolconfigs.schema.json | 5 + .../crds/virtualmcpservers.schema.json | 167 +++++++++++++++++- static/api-specs/toolhive-api.yaml | 142 +++++++++++---- .../api-specs/upstream-registry.schema.json | 30 ---- 10 files changed, 322 insertions(+), 67 deletions(-) diff --git a/docs/toolhive/reference/cli/thv_client_register.md b/docs/toolhive/reference/cli/thv_client_register.md index 41c42e0a..fda1e600 100644 --- a/docs/toolhive/reference/cli/thv_client_register.md +++ b/docs/toolhive/reference/cli/thv_client_register.md @@ -28,6 +28,7 @@ Valid clients: - cline: VS Code Cline extension - codex: OpenAI Codex CLI - continue: Continue.dev IDE plugins + - copilot-cli: GitHub Copilot CLI - cursor: Cursor editor - factory: Factory.ai Droid CLI - gemini-cli: Google Gemini CLI diff --git a/docs/toolhive/reference/cli/thv_client_remove.md b/docs/toolhive/reference/cli/thv_client_remove.md index f1d6044c..f32115fc 100644 --- a/docs/toolhive/reference/cli/thv_client_remove.md +++ b/docs/toolhive/reference/cli/thv_client_remove.md @@ -28,6 +28,7 @@ Valid clients: - cline: VS Code Cline extension - codex: OpenAI Codex CLI - continue: Continue.dev IDE plugins + - copilot-cli: GitHub Copilot CLI - cursor: Cursor editor - factory: Factory.ai Droid CLI - gemini-cli: Google Gemini CLI diff --git a/docs/toolhive/reference/cli/thv_run.md b/docs/toolhive/reference/cli/thv_run.md index d3c38dc4..b69d7b6c 100644 --- a/docs/toolhive/reference/cli/thv_run.md +++ b/docs/toolhive/reference/cli/thv_run.md @@ -178,6 +178,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] --runtime-add-package stringArray Add additional packages to install in the builder and runtime stages (can be repeated) --runtime-image string Override the default base image for protocol schemes (e.g., golang:1.24-alpine, node:20-alpine, python:3.11-slim) --secret stringArray Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET) + --session-ttl duration Session inactivity timeout (e.g., 30m, 2h); zero uses the default (2h) --stateless Declare the server as stateless (POST-only, no SSE). Use for MCP servers implementing streamable-HTTP stateless mode. --target-host string Host to forward traffic to (only applicable to SSE or Streamable HTTP transport) (default "127.0.0.1") --target-port int Port for the container to expose (only applicable to SSE or Streamable HTTP transport) diff --git a/docs/toolhive/reference/cli/thv_vmcp_serve.md b/docs/toolhive/reference/cli/thv_vmcp_serve.md index f20718f6..48243c7e 100644 --- a/docs/toolhive/reference/cli/thv_vmcp_serve.md +++ b/docs/toolhive/reference/cli/thv_vmcp_serve.md @@ -42,6 +42,7 @@ thv vmcp serve [flags] --optimizer Enable FTS5 keyword optimizer (Tier 1): exposes find_tool and call_tool instead of all backend tools --optimizer-embedding Enable managed TEI semantic optimizer (Tier 2); implies --optimizer --port int Port to listen on (default 4483) + --session-ttl duration Session inactivity timeout (e.g., 30m, 2h); zero uses the default (30m) ``` ### Options inherited from parent commands diff --git a/static/api-specs/crds/mcpexternalauthconfigs.schema.json b/static/api-specs/crds/mcpexternalauthconfigs.schema.json index 7eff2275..25cc8243 100644 --- a/static/api-specs/crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/crds/mcpexternalauthconfigs.schema.json @@ -529,6 +529,33 @@ } ] }, + "identityFromToken": { + "description": "IdentityFromToken extracts user identity (subject, name, email) directly\nfrom the OAuth2 token-endpoint response body using gjson dot-notation paths.\nWhen set, the embedded auth server skips the userinfo HTTP call entirely\nand resolves identity from the token response. See IdentityFromTokenConfig\nfor trust-model and uniqueness considerations.", + "properties": { + "emailPath": { + "description": "EmailPath is the dot-notation path to the email address field in the token response.\nIf not specified or if the path does not resolve to a string, the email is omitted.\nOmit the field entirely rather than setting it to an empty string.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "namePath": { + "description": "NamePath is the dot-notation path to the display name field in the token response.\nIf not specified or if the path does not resolve to a string, the display name is omitted.\nOmit the field entirely rather than setting it to an empty string.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "subjectPath": { + "description": "SubjectPath is the dot-notation path to the subject (user ID) field in the token response.\nWarning: claims read from the token response are trusted only via TLS, not\ncryptographically verified; prefer OIDC ID tokens when verifiable claims are required.\nExample: \"authed_user.id\" for Slack (top-level token-response field). For providers\nwhose token response embeds the access token as a JWT (e.g. Snowflake), use the\n\"@upstreamjwt\" modifier to decode the payload, e.g. \"access_token|@upstreamjwt|sub\".\nThe \"@upstreamjwt\" modifier performs no signature verification either.", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "subjectPath" + ], + "type": "object" + }, "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" @@ -547,7 +574,7 @@ "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.", + "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": { "accessTokenPath": { "description": "AccessTokenPath is the dot-notation path to the access token in the response.\nExample: \"authed_user.access_token\"", @@ -573,7 +600,7 @@ "type": "object" }, "userInfo": { - "description": "UserInfo contains configuration for fetching user information from the upstream provider.\nWhen omitted, the embedded auth server runs in synthesis mode for this\nupstream: a non-PII subject derived from the access token, no Name/Email.\nUse this shape for upstreams with no userinfo surface (e.g., MCP\nauthorization servers per the MCP spec).", + "description": "UserInfo contains configuration for fetching user information from the upstream provider.\nWhen omitted and IdentityFromToken is also unset, the embedded auth server runs in\nsynthesis mode for this upstream: a non-PII subject derived from the access token, no\nName/Email. Use this shape for upstreams with no userinfo surface and no identity in\nthe token response (e.g., MCP authorization servers per the MCP spec). When\nIdentityFromToken is set instead, identity is resolved from the token response body\n(e.g., Snowflake's \"username\" field, Slack's \"authed_user.id\"); the userinfo HTTP call\nis skipped entirely.", "properties": { "additionalHeaders": { "additionalProperties": { @@ -1027,6 +1054,11 @@ "format": "int64", "type": "integer" }, + "referenceCount": { + "description": "ReferenceCount is the number of workloads referencing this config.", + "format": "int32", + "type": "integer" + }, "referencingWorkloads": { "description": "ReferencingWorkloads is a list of workload resources that reference this MCPExternalAuthConfig.\nEach entry identifies the workload by kind and name.", "items": { diff --git a/static/api-specs/crds/mcpoidcconfigs.schema.json b/static/api-specs/crds/mcpoidcconfigs.schema.json index 158e894b..f06f0aba 100644 --- a/static/api-specs/crds/mcpoidcconfigs.schema.json +++ b/static/api-specs/crds/mcpoidcconfigs.schema.json @@ -231,6 +231,11 @@ "format": "int64", "type": "integer" }, + "referenceCount": { + "description": "ReferenceCount is the number of workloads referencing this config.", + "format": "int32", + "type": "integer" + }, "referencingWorkloads": { "description": "ReferencingWorkloads is a list of workload resources that reference this MCPOIDCConfig.\nEach entry identifies the workload by kind and name.", "items": { diff --git a/static/api-specs/crds/mcptoolconfigs.schema.json b/static/api-specs/crds/mcptoolconfigs.schema.json index 9efb42fc..a4c8354f 100644 --- a/static/api-specs/crds/mcptoolconfigs.schema.json +++ b/static/api-specs/crds/mcptoolconfigs.schema.json @@ -141,6 +141,11 @@ "format": "int64", "type": "integer" }, + "referenceCount": { + "description": "ReferenceCount is the number of workloads referencing this config.", + "format": "int32", + "type": "integer" + }, "referencingWorkloads": { "description": "ReferencingWorkloads is a list of workload resources that reference this MCPToolConfig.\nEach entry identifies the workload by kind and name.", "items": { diff --git a/static/api-specs/crds/virtualmcpservers.schema.json b/static/api-specs/crds/virtualmcpservers.schema.json index b81a5eb5..6156cab6 100644 --- a/static/api-specs/crds/virtualmcpservers.schema.json +++ b/static/api-specs/crds/virtualmcpservers.schema.json @@ -420,6 +420,33 @@ } ] }, + "identityFromToken": { + "description": "IdentityFromToken extracts user identity (subject, name, email) directly\nfrom the OAuth2 token-endpoint response body using gjson dot-notation paths.\nWhen set, the embedded auth server skips the userinfo HTTP call entirely\nand resolves identity from the token response. See IdentityFromTokenConfig\nfor trust-model and uniqueness considerations.", + "properties": { + "emailPath": { + "description": "EmailPath is the dot-notation path to the email address field in the token response.\nIf not specified or if the path does not resolve to a string, the email is omitted.\nOmit the field entirely rather than setting it to an empty string.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "namePath": { + "description": "NamePath is the dot-notation path to the display name field in the token response.\nIf not specified or if the path does not resolve to a string, the display name is omitted.\nOmit the field entirely rather than setting it to an empty string.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "subjectPath": { + "description": "SubjectPath is the dot-notation path to the subject (user ID) field in the token response.\nWarning: claims read from the token response are trusted only via TLS, not\ncryptographically verified; prefer OIDC ID tokens when verifiable claims are required.\nExample: \"authed_user.id\" for Slack (top-level token-response field). For providers\nwhose token response embeds the access token as a JWT (e.g. Snowflake), use the\n\"@upstreamjwt\" modifier to decode the payload, e.g. \"access_token|@upstreamjwt|sub\".\nThe \"@upstreamjwt\" modifier performs no signature verification either.", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "subjectPath" + ], + "type": "object" + }, "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" @@ -438,7 +465,7 @@ "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.", + "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": { "accessTokenPath": { "description": "AccessTokenPath is the dot-notation path to the access token in the response.\nExample: \"authed_user.access_token\"", @@ -464,7 +491,7 @@ "type": "object" }, "userInfo": { - "description": "UserInfo contains configuration for fetching user information from the upstream provider.\nWhen omitted, the embedded auth server runs in synthesis mode for this\nupstream: a non-PII subject derived from the access token, no Name/Email.\nUse this shape for upstreams with no userinfo surface (e.g., MCP\nauthorization servers per the MCP spec).", + "description": "UserInfo contains configuration for fetching user information from the upstream provider.\nWhen omitted and IdentityFromToken is also unset, the embedded auth server runs in\nsynthesis mode for this upstream: a non-PII subject derived from the access token, no\nName/Email. Use this shape for upstreams with no userinfo surface and no identity in\nthe token response (e.g., MCP authorization servers per the MCP spec). When\nIdentityFromToken is set instead, identity is resolved from the token response body\n(e.g., Snowflake's \"username\" field, Slack's \"authed_user.id\"); the userinfo HTTP call\nis skipped entirely.", "properties": { "additionalHeaders": { "additionalProperties": { @@ -1748,6 +1775,126 @@ ], "type": "object" }, + "rateLimiting": { + "description": "RateLimiting defines rate limiting configuration for the Virtual MCP server.\nRequires Redis session storage to be configured for distributed rate limiting.", + "properties": { + "perUser": { + "description": "PerUser is a token bucket applied independently to each authenticated user\nat the server level. Requires authentication to be enabled.\nEach unique userID creates Redis keys that expire after 2x refillPeriod.\nMemory formula: unique_users_per_TTL_window * (1 + num_tools_with_per_user_limits) keys.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object" + }, + "shared": { + "description": "Shared is a token bucket shared across all users for the entire server.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object" + }, + "tools": { + "description": "Tools defines per-tool rate limit overrides.\nEach entry applies additional rate limits to calls targeting a specific tool name.\nA request must pass both the server-level limit and the per-tool limit.", + "items": { + "description": "ToolRateLimitConfig defines rate limits for a specific tool.\nAt least one of shared or perUser must be configured.", + "properties": { + "name": { + "description": "Name is the MCP tool name this limit applies to.", + "minLength": 1, + "type": "string" + }, + "perUser": { + "description": "PerUser token bucket configuration for this tool.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object" + }, + "shared": { + "description": "Shared token bucket for this specific tool.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of shared or perUser must be configured", + "rule": "has(self.shared) || has(self.perUser)" + } + ] + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of shared, perUser, or tools must be configured", + "rule": "has(self.shared) || has(self.perUser) || (has(self.tools) && size(self.tools) > 0)" + } + ] + }, "sessionStorage": { "description": "SessionStorage configures session storage for stateful horizontal scaling.\nWhen provider is \"redis\", the operator injects Redis connection parameters\n(address, db, keyPrefix) here. The Redis password is provided separately via\nthe THV_SESSION_REDIS_PASSWORD environment variable.", "properties": { @@ -2225,7 +2372,21 @@ "groupRef", "incomingAuth" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "config.rateLimiting requires sessionStorage with provider 'redis'", + "rule": "!has(self.config) || !has(self.config.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')" + }, + { + "message": "config.rateLimiting.perUser requires incomingAuth.type oidc", + "rule": "!(has(self.config) && has(self.config.rateLimiting) && has(self.config.rateLimiting.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')" + }, + { + "message": "per-tool perUser rate limiting requires incomingAuth.type oidc", + "rule": "!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools) || self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')" + } + ] }, "status": { "description": "VirtualMCPServerStatus defines the observed state of VirtualMCPServer", diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 250d82d1..6e9d446e 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -31,31 +31,15 @@ components: description: Version is the schema version of the registry type: string type: object - github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket: - description: |- - PerUser token bucket configuration for this tool. - +optional - properties: - maxTokens: - description: |- - MaxTokens is the maximum number of tokens (bucket capacity). - This is also the burst size: the maximum number of requests that can be served - instantaneously before the bucket is depleted. - +kubebuilder:validation:Required - +kubebuilder:validation:Minimum=1 - type: integer - refillPeriod: - $ref: '#/components/schemas/v1.Duration' - type: object github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitConfig: description: |- RateLimitConfig contains the CRD rate limiting configuration. When set, rate limiting middleware is added to the proxy middleware chain. properties: perUser: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_ratelimit_types.RateLimitBucket' shared: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_ratelimit_types.RateLimitBucket' tools: description: |- Tools defines per-tool rate limit overrides. @@ -65,23 +49,10 @@ components: +listMapKey=name +optional items: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.ToolRateLimitConfig' + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_ratelimit_types.ToolRateLimitConfig' type: array uniqueItems: false type: object - github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.ToolRateLimitConfig: - properties: - name: - description: |- - Name is the MCP tool name this limit applies to. - +kubebuilder:validation:Required - +kubebuilder:validation:MinLength=1 - type: string - perUser: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' - shared: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' - type: object github_com_stacklok_toolhive_pkg_audit.Config: description: |- DEPRECATED: Middleware configuration. @@ -439,6 +410,26 @@ components: server trusts. type: string type: object + github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig: + description: |- + IdentityFromToken extracts user identity (subject, name, email) directly from the + OAuth2 token-endpoint response body using gjson dot-notation paths. When set, the + embedded auth server skips the userinfo HTTP call entirely. Mirrors the CRD type + (cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative + trust-model and uniqueness documentation lives there. + properties: + email_path: + description: EmailPath is the dot-notation path to the email address field. + type: string + name_path: + description: NamePath is the dot-notation path to the display name field. + type: string + subject_path: + description: |- + SubjectPath is the dot-notation path to the subject (user ID) field. + Required when IdentityFromToken is set. + type: string + type: object github_com_stacklok_toolhive_pkg_authserver.OAuth2UpstreamRunConfig: description: |- OAuth2Config contains OAuth 2.0-specific configuration. @@ -474,6 +465,8 @@ components: type: string dcr_config: $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.DCRUpstreamConfig' + identity_from_token: + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig' redirect_uri: description: |- RedirectURI is the callback URL where the upstream IDP will redirect after authentication. @@ -909,6 +902,7 @@ components: - codex - kimi-cli - factory + - copilot-cli type: string x-enum-varnames: - RooCode @@ -938,6 +932,7 @@ components: - Codex - KimiCli - Factory + - CopilotCli github_com_stacklok_toolhive_pkg_client.ClientAppStatus: properties: client_type: @@ -1157,6 +1152,35 @@ components: description: TokenURL is the OAuth 2.0 token endpoint URL type: string type: object + github_com_stacklok_toolhive_pkg_ratelimit_types.RateLimitBucket: + description: |- + PerUser token bucket configuration for this tool. + +optional + properties: + maxTokens: + description: |- + MaxTokens is the maximum number of tokens (bucket capacity). + This is also the burst size: the maximum number of requests that can be served + instantaneously before the bucket is depleted. + +kubebuilder:validation:Required + +kubebuilder:validation:Minimum=1 + type: integer + refillPeriod: + $ref: '#/components/schemas/v1.Duration' + type: object + github_com_stacklok_toolhive_pkg_ratelimit_types.ToolRateLimitConfig: + properties: + name: + description: |- + Name is the MCP tool name this limit applies to. + +kubebuilder:validation:Required + +kubebuilder:validation:MinLength=1 + type: string + perUser: + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_ratelimit_types.RateLimitBucket' + shared: + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_ratelimit_types.RateLimitBucket' + type: object github_com_stacklok_toolhive_pkg_registry.OAuthPublicConfig: description: |- AuthConfig contains the non-secret OAuth configuration when auth is configured. @@ -1370,6 +1394,16 @@ components: type: string type: array uniqueItems: false + session_ttl: + description: |- + SessionTTL is the inactivity timeout for proxy sessions, expressed as a Go + duration string (e.g. "30m", "2h", "168h"). Empty uses the transport + default (2h). Negative durations and values that fail time.ParseDuration + are rejected at runtime. + String (not time.Duration) keeps the wire format unit-explicit: a + time.Duration field serializes as nanoseconds in JSON. + example: 2h + type: string stateless: description: |- Stateless indicates the server only supports POST (no SSE/GET). @@ -2047,6 +2081,7 @@ components: description: Version is the package version (required for npm, pypi, nuget; optional for mcpb; not used by oci where version is in the identifier) example: 1.0.2 + maxLength: 255 minLength: 1 type: string type: object @@ -3524,6 +3559,8 @@ components: type: string version: example: 1.0.2 + maxLength: 255 + minLength: 1 type: string websiteUrl: example: https://modelcontextprotocol.io/examples @@ -4014,6 +4051,47 @@ paths: summary: Update registry configuration tags: - registry + /api/v1beta/registry/{name}/refresh: + post: + description: Force a refresh of the server-side registry cache for the default + registry + parameters: + - description: Registry name (must be 'default') + in: path + name: name + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Registry refreshed + "404": + content: + application/json: + schema: + type: string + description: Not Found + "500": + content: + application/json: + schema: + type: string + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: '#/components/schemas/pkg_api_v1.registryErrorResponse' + description: Registry authentication required or upstream registry unavailable + summary: Refresh registry cache + tags: + - registry /api/v1beta/registry/{name}/servers: get: description: Get a list of servers in a specific registry diff --git a/static/api-specs/upstream-registry.schema.json b/static/api-specs/upstream-registry.schema.json index cc83ad78..f89f7b0f 100644 --- a/static/api-specs/upstream-registry.schema.json +++ b/static/api-specs/upstream-registry.schema.json @@ -49,36 +49,6 @@ "description": "MCP server object (see MCP server object schema section below). Source: https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json" } }, - "groups": { - "type": "array", - "description": "Groups of related MCP servers", - "items": { - "type": "object", - "required": [ - "name", - "description", - "servers" - ], - "properties": { - "name": { - "type": "string", - "description": "Unique identifier for the group" - }, - "description": { - "type": "string", - "description": "Description of the group's purpose" - }, - "servers": { - "type": "array", - "description": "Array of servers in this group", - "items": { - "type": "object", - "description": "MCP server object (see MCP server object schema section below). Source: https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json" - } - } - } - } - }, "skills": { "type": "array", "description": "Array of skills in the registry", From 89b4a45de9db9d09a2ceb9e7094175cea5676ffd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:49:35 +0000 Subject: [PATCH 3/6] Document v0.28.0 user-facing features Cover the toolhive v0.28.0 changes that need hand-written docs: Copilot CLI client, identityFromToken on OAuth2 upstreams, VirtualMCPServer rate limiting, --session-ttl flag, and the new TOOLHIVE_SKIP_UPDATE_CHECK env var. Co-authored-by: Unknown --- SUMMARY.md | 22 +++++++ docs/toolhive/faq.mdx | 15 +++++ docs/toolhive/guides-cli/run-mcp-servers.mdx | 13 ++++ docs/toolhive/guides-k8s/auth-k8s.mdx | 54 ++++++++++++++-- docs/toolhive/guides-k8s/rate-limiting.mdx | 61 +++++++++++++++++-- docs/toolhive/guides-vmcp/authentication.mdx | 11 ++++ docs/toolhive/guides-vmcp/local-cli.mdx | 1 + .../guides-vmcp/scaling-and-performance.mdx | 8 ++- .../reference/client-compatibility.mdx | 23 +++++++ 9 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 SUMMARY.md diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 00000000..e62d715c --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,22 @@ +# Summary of changes + +- Added GitHub Copilot CLI to the supported clients table and a new + "Configuration locations" section in `docs/toolhive/reference/client-compatibility.mdx` + (`stacklok/toolhive#5287`). +- Added a new "Extract identity from the token response" section in + `docs/toolhive/guides-k8s/auth-k8s.mdx` covering the `identityFromToken` + OAuth 2.0 upstream field, with a cross-link tip in + `docs/toolhive/guides-vmcp/authentication.mdx` (`stacklok/toolhive#5269`, + `stacklok/toolhive#5222`, `stacklok/toolhive#5285`). +- Added a "Rate limit a VirtualMCPServer" section in + `docs/toolhive/guides-k8s/rate-limiting.mdx` covering the new + `spec.config.rateLimiting` field, with prerequisites for Redis session + storage and OIDC incoming auth (`stacklok/toolhive#5079`). +- Documented the new `--session-ttl` flag in `docs/toolhive/guides-cli/run-mcp-servers.mdx`, + the `thv vmcp serve` flag reference in `docs/toolhive/guides-vmcp/local-cli.mdx`, + and the vMCP TTL discussion in + `docs/toolhive/guides-vmcp/scaling-and-performance.mdx` + (`stacklok/toolhive#5117`). +- Added a "How do I disable update checks?" FAQ entry covering the new + `TOOLHIVE_SKIP_UPDATE_CHECK` environment variable in + `docs/toolhive/faq.mdx` (`stacklok/toolhive#5264`). diff --git a/docs/toolhive/faq.mdx b/docs/toolhive/faq.mdx index 0eb025f1..bdda738a 100644 --- a/docs/toolhive/faq.mdx +++ b/docs/toolhive/faq.mdx @@ -240,6 +240,21 @@ export TOOLHIVE_USAGE_METRICS_ENABLED=false Once you opt out, ToolHive stops collecting and sending usage metrics. You need to restart any running servers for the change to take effect. +### How do I disable update checks? + +ToolHive periodically checks for new versions. To disable this check (and the +usage-metrics collection it gates), set the `TOOLHIVE_SKIP_UPDATE_CHECK` +environment variable to `true`: + +```bash +export TOOLHIVE_SKIP_UPDATE_CHECK=true +``` + +The setting is honored by the CLI, the API server, and the Kubernetes operator +telemetry service. For the operator, add it to the `operator.env` list in your +Helm values. Update checks are also skipped automatically when ToolHive detects +a CI environment. + ## Security and permissions ### Is it safe to run MCP servers? diff --git a/docs/toolhive/guides-cli/run-mcp-servers.mdx b/docs/toolhive/guides-cli/run-mcp-servers.mdx index 0314ea63..e08a58ad 100644 --- a/docs/toolhive/guides-cli/run-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/run-mcp-servers.mdx @@ -248,6 +248,19 @@ specific proxy port instead, use the `--proxy-port` flag: thv run --proxy-port ``` +### Override the session timeout + +ToolHive's proxy evicts idle MCP sessions after 2 hours by default. To raise or +lower this inactivity timeout for a workload, pass `--session-ttl` with a Go +duration string: + +```bash +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. + ### Run a server exposing only selected tools ToolHive can filter the tools returned to the client as result of a `tools/list` diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index 96f81a36..23f7fd16 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -779,15 +779,59 @@ from the configured endpoint, and the `fieldMapping` section maps provider-specific response fields to standard user identity fields (for example, GitHub returns `login` instead of the standard `name` field). -When you omit `userInfo`, the embedded auth server runs in synthesis mode for -this upstream: it derives a non-personally-identifying subject (with a `tk-` -prefix) from the access token and leaves `name` and `email` empty. Use this -configuration for OAuth 2.0 servers that don't expose a userinfo endpoint, such -as MCP authorization servers that comply with the +When you omit `userInfo` and `identityFromToken`, the embedded auth server runs +in synthesis mode for this upstream: it derives a non-personally-identifying +subject (with a `tk-` prefix) from the access token and leaves `name` and +`email` empty. Use this configuration for OAuth 2.0 servers that don't expose a +userinfo endpoint and don't return identity in the token response, such as MCP +authorization servers that comply with the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). ::: +#### Extract identity from the token response + +Some providers don't expose a userinfo endpoint but return user identity in the +OAuth 2.0 token response itself. For example, Slack returns the user ID at +`authed_user.id`, and Snowflake embeds the username in the access token as a +JWT. For these providers, set `identityFromToken` on `oauth2Config` instead of +`userInfo`. The embedded auth server then skips the userinfo HTTP call and +resolves identity from gjson dot-notation paths into the token response body. + +```yaml title="oauth2Config snippet for Slack-style providers" +oauth2Config: + # highlight-start + identityFromToken: + subjectPath: authed_user.id + namePath: authed_user.name + emailPath: authed_user.email + # highlight-end +``` + +For providers like Snowflake that embed identity in a JWT inside the token +response, use the `@upstreamjwt` modifier to decode the payload: + +```yaml title="oauth2Config snippet for JWT-embedded identity" +oauth2Config: + # highlight-start + identityFromToken: + subjectPath: 'access_token|@upstreamjwt|sub' + # highlight-end +``` + +`subjectPath` is required; `namePath` and `emailPath` are optional. Each path +must be 1-256 characters long. `identityFromToken` and `userInfo` can both be +set, but only one is used at runtime. + +:::warning[Trust model] + +Claims read from the token response are trusted via TLS only and are not +cryptographically verified. The `@upstreamjwt` modifier decodes the JWT payload +without verifying its signature. Prefer OIDC ID tokens when you need +cryptographically verifiable claims. + +::: + ### Upstream-specific authorization parameters Some identity providers require custom query parameters on the authorization URL diff --git a/docs/toolhive/guides-k8s/rate-limiting.mdx b/docs/toolhive/guides-k8s/rate-limiting.mdx index 50eca2ff..58d016ef 100644 --- a/docs/toolhive/guides-k8s/rate-limiting.mdx +++ b/docs/toolhive/guides-k8s/rate-limiting.mdx @@ -1,14 +1,14 @@ --- title: Rate limiting description: - Configure per-user and shared rate limits on MCPServer resources to prevent - noisy neighbors and protect downstream services. + Configure per-user and shared rate limits on MCPServer and VirtualMCPServer + resources to prevent noisy neighbors and protect downstream services. --- -Configure token bucket rate limits on MCPServer resources to control how many -tool invocations users can make. Rate limiting prevents individual users from -monopolizing shared servers and protects downstream services from traffic -spikes. +Configure token bucket rate limits on MCPServer and VirtualMCPServer resources +to control how many tool invocations users can make. Rate limiting prevents +individual users from monopolizing shared servers and protects downstream +services from traffic spikes. ToolHive supports two scopes of rate limiting: @@ -219,6 +219,55 @@ In this example: also count toward the 100 server-level limit). - All users combined can make 50 `shared_resource` calls per minute. +## Rate limit a VirtualMCPServer + +VirtualMCPServer resources accept the same rate limit shape under +`spec.config.rateLimiting`. The fields and token bucket semantics match the +MCPServer examples above, but the prerequisites are stricter: + +- `spec.sessionStorage.provider` must be `redis`. The CRD rejects any + `rateLimiting` configuration without Redis-backed session storage. +- `spec.incomingAuth.type` must be `oidc` when you configure any per-user + bucket - either at the server level or on a per-tool override. + +A request must pass both the server-level vMCP limit and the per-tool limit (if +defined). Limits apply to the vMCP aggregator and are independent from any +limits configured on the backend MCPServers it routes to. + +```yaml title="vmcp-ratelimit.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: VirtualMCPServer +metadata: + name: shared-toolkit + namespace: toolhive-system +spec: + groupRef: + name: my-backends + incomingAuth: + type: oidc + oidcConfigRef: + name: my-oidc-config + audience: shared-toolkit + sessionStorage: + provider: redis + address: + config: + # highlight-start + rateLimiting: + shared: + maxTokens: 5000 + refillPeriod: 1m0s + perUser: + maxTokens: 200 + refillPeriod: 1m0s + tools: + - name: expensive_search + perUser: + maxTokens: 20 + refillPeriod: 1m0s + # highlight-end +``` + ## Next steps - [Token exchange](./token-exchange-k8s.mdx) to configure token exchange for diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 12124b59..13fac9fa 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -491,6 +491,17 @@ at `authed_user.access_token`). Add a `tokenResponseMapping` block to the ::: +:::tip[Identity in the token response] + +When an upstream returns user identity in the token response itself (Slack +returns it at `authed_user.id`; Snowflake embeds it in the access-token JWT), +set `identityFromToken` on the `oauth2Config` with gjson dot-notation paths for +`subjectPath` (required), `namePath`, and `emailPath`. See +[Extract identity from the token response](../guides-k8s/auth-k8s.mdx#extract-identity-from-the-token-response) +for the full pattern and trust-model caveats. + +::: + ### Incoming auth with the embedded auth server When using the embedded auth server, configure `incomingAuth` to validate the diff --git a/docs/toolhive/guides-vmcp/local-cli.mdx b/docs/toolhive/guides-vmcp/local-cli.mdx index 22059893..9c3e6695 100644 --- a/docs/toolhive/guides-vmcp/local-cli.mdx +++ b/docs/toolhive/guides-vmcp/local-cli.mdx @@ -272,6 +272,7 @@ All `thv vmcp` flags, with their defaults: | `--optimizer-embedding` | `false` | Enable Tier 2 semantic optimizer (implies `--optimizer`) | | `--embedding-model` | `BAAI/bge-small-en-v1.5` | HuggingFace model name for the managed TEI container | | `--embedding-image` | `ghcr.io/huggingface/text-embeddings-inference:cpu-latest` | TEI container image | +| `--session-ttl` | `30m` | Session inactivity timeout as a Go duration (`30m`, `2h`, `168h`) | ### `thv vmcp init` diff --git a/docs/toolhive/guides-vmcp/scaling-and-performance.mdx b/docs/toolhive/guides-vmcp/scaling-and-performance.mdx index 5a163ef7..6f69a9aa 100644 --- a/docs/toolhive/guides-vmcp/scaling-and-performance.mdx +++ b/docs/toolhive/guides-vmcp/scaling-and-performance.mdx @@ -159,9 +159,11 @@ configure Redis session storage. Total capacity scales as `replicas × 1,000`. ### Session time-to-live (TTL) -The vMCP server applies a **30-minute inactivity TTL** to session metadata. A -session that receives no activity for 30 minutes expires, and the client must -reinitialize it. +The vMCP server applies a **30-minute inactivity TTL** to session metadata by +default. A session that receives no activity for the TTL window expires, and the +client must reinitialize it. When running locally with `thv vmcp serve`, pass +`--session-ttl` (Go duration, for example `--session-ttl=2h`) to raise or lower +this default. With Redis session storage, the TTL is a sliding window: every request atomically refreshes the key's expiry. Active sessions remain valid indefinitely diff --git a/docs/toolhive/reference/client-compatibility.mdx b/docs/toolhive/reference/client-compatibility.mdx index d1f28179..79dfe24e 100644 --- a/docs/toolhive/reference/client-compatibility.mdx +++ b/docs/toolhive/reference/client-compatibility.mdx @@ -15,6 +15,7 @@ We've tested ToolHive with these clients: | Client | Supported | Auto-configuration | Skills support | Notes | | -------------------------- | :-------: | :----------------: | :------------: | ------------------------------------------- | | GitHub Copilot (VS Code) | ✅ | ✅ | ✅ | v1.102+ or Insiders version ([see note][3]) | +| GitHub Copilot CLI | ✅ | ✅ | ❌ | | | Claude Code | ✅ | ✅ | ✅ | v1.0.27+ | | Cursor | ✅ | ✅ | ✅ | v0.50.0+ | | Cline (VS Code) | ✅ | ✅ | ✅ | v3.17.10+ | @@ -281,6 +282,28 @@ global MCP configuration file whenever you run an MCP server. You can also configure project-specific MCP servers by creating a `.continue/mcpServers/.yaml` file in your project directory. +### GitHub Copilot CLI + +The [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) +stores its MCP configuration in a JSON file in your home directory. + +- **All platforms**: `~/.copilot/mcp-config.json` + +Example configuration: + +```json +{ + "mcpServers": { + "github": { "url": "http://localhost:19046/mcp", "type": "http" }, + "fetch": { "url": "http://localhost:43832/mcp", "type": "http" }, + "sqlite": { "url": "http://localhost:51712/sse#sqlite", "type": "sse" } + } +} +``` + +When you register the Copilot CLI as a client, ToolHive automatically updates +this file whenever you run an MCP server. + ## Manual configuration If your client doesn't support automatic configuration, you'll need to set up From ac1238b301406a656ec56b1c5b5e1b6334f8a08a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:59:26 +0000 Subject: [PATCH 4/6] Clarify identityFromToken behavior in auth-k8s docs Fix awkward "from X into Y" phrasing and make precedence explicit when both identityFromToken and userInfo are set on an OAuth2 upstream. --- docs/toolhive/guides-k8s/auth-k8s.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index 23f7fd16..b34dc4ef 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -796,7 +796,7 @@ OAuth 2.0 token response itself. For example, Slack returns the user ID at `authed_user.id`, and Snowflake embeds the username in the access token as a JWT. For these providers, set `identityFromToken` on `oauth2Config` instead of `userInfo`. The embedded auth server then skips the userinfo HTTP call and -resolves identity from gjson dot-notation paths into the token response body. +extracts identity from the token response body using gjson dot-notation paths. ```yaml title="oauth2Config snippet for Slack-style providers" oauth2Config: @@ -820,8 +820,9 @@ oauth2Config: ``` `subjectPath` is required; `namePath` and `emailPath` are optional. Each path -must be 1-256 characters long. `identityFromToken` and `userInfo` can both be -set, but only one is used at runtime. +must be 1-256 characters long. If you set both `identityFromToken` and +`userInfo`, `identityFromToken` takes precedence and the userinfo HTTP call is +skipped. :::warning[Trust model] From 370e809373590c5c3e1bc4955a164aed697beb9a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 19:00:05 +0000 Subject: [PATCH 5/6] Add upstream-release-docs content for toolhive v0.28.0 --- SUMMARY.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 SUMMARY.md diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index e62d715c..00000000 --- a/SUMMARY.md +++ /dev/null @@ -1,22 +0,0 @@ -# Summary of changes - -- Added GitHub Copilot CLI to the supported clients table and a new - "Configuration locations" section in `docs/toolhive/reference/client-compatibility.mdx` - (`stacklok/toolhive#5287`). -- Added a new "Extract identity from the token response" section in - `docs/toolhive/guides-k8s/auth-k8s.mdx` covering the `identityFromToken` - OAuth 2.0 upstream field, with a cross-link tip in - `docs/toolhive/guides-vmcp/authentication.mdx` (`stacklok/toolhive#5269`, - `stacklok/toolhive#5222`, `stacklok/toolhive#5285`). -- Added a "Rate limit a VirtualMCPServer" section in - `docs/toolhive/guides-k8s/rate-limiting.mdx` covering the new - `spec.config.rateLimiting` field, with prerequisites for Redis session - storage and OIDC incoming auth (`stacklok/toolhive#5079`). -- Documented the new `--session-ttl` flag in `docs/toolhive/guides-cli/run-mcp-servers.mdx`, - the `thv vmcp serve` flag reference in `docs/toolhive/guides-vmcp/local-cli.mdx`, - and the vMCP TTL discussion in - `docs/toolhive/guides-vmcp/scaling-and-performance.mdx` - (`stacklok/toolhive#5117`). -- Added a "How do I disable update checks?" FAQ entry covering the new - `TOOLHIVE_SKIP_UPDATE_CHECK` environment variable in - `docs/toolhive/faq.mdx` (`stacklok/toolhive#5264`). From 74412f2d744d5e8e5773fd20e73721c319ddf453 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 19 May 2026 23:19:31 +0100 Subject: [PATCH 6/6] Fix identityFromToken examples in auth-k8s docs Snowflake returns the login name as a top-level username field, not as a JWT claim inside access_token. Replace the @upstreamjwt example with the actual top-level path, keep @upstreamjwt as a generic capability. Slack's oauth.v2.access response only returns authed_user.id; drop the fabricated authed_user.name and authed_user.email fields. Also link to the gjson path syntax, promote the section to H3 with a back-link from the synthesis-mode note, and clarify that extraction failure has no fallback to userInfo. Co-Authored-By: Claude Opus 4.7 --- docs/toolhive/guides-k8s/auth-k8s.mdx | 51 +++++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index b34dc4ef..e145570f 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -786,30 +786,48 @@ subject (with a `tk-` prefix) from the access token and leaves `name` and userinfo endpoint and don't return identity in the token response, such as MCP authorization servers that comply with the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). +For OAuth 2.0 servers that return identity in the token response itself, see +[Extract identity from the token response](#extract-identity-from-the-token-response). ::: -#### Extract identity from the token response +### Extract identity from the token response Some providers don't expose a userinfo endpoint but return user identity in the -OAuth 2.0 token response itself. For example, Slack returns the user ID at -`authed_user.id`, and Snowflake embeds the username in the access token as a -JWT. For these providers, set `identityFromToken` on `oauth2Config` instead of -`userInfo`. The embedded auth server then skips the userinfo HTTP call and -extracts identity from the token response body using gjson dot-notation paths. +OAuth 2.0 token response itself. For these providers, set `identityFromToken` on +`oauth2Config` instead of `userInfo`. The embedded auth server then skips the +userinfo HTTP call and extracts identity from the token response body using +[gjson dot-notation paths](https://github.com/tidwall/gjson#path-syntax): +`username` extracts a top-level field, `authed_user.id` extracts a nested field, +and the pipe operator chains modifiers like `@upstreamjwt`. -```yaml title="oauth2Config snippet for Slack-style providers" +For example, Slack's `oauth.v2.access` response includes the authenticated user +ID at `authed_user.id`: + +```yaml title="oauth2Config snippet for Slack" oauth2Config: # highlight-start identityFromToken: subjectPath: authed_user.id - namePath: authed_user.name - emailPath: authed_user.email # highlight-end ``` -For providers like Snowflake that embed identity in a JWT inside the token -response, use the `@upstreamjwt` modifier to decode the payload: +Snowflake returns the authenticated login name as a top-level `username` field +in every authorization-code grant response, and does not expose a userinfo +endpoint: + +```yaml title="oauth2Config snippet for Snowflake" +oauth2Config: + # highlight-start + identityFromToken: + subjectPath: username + namePath: username + # highlight-end +``` + +For providers whose token response embeds identity inside a JWT-shaped access +token, the `@upstreamjwt` modifier decodes the JWT payload so subsequent path +segments can drill into it: ```yaml title="oauth2Config snippet for JWT-embedded identity" oauth2Config: @@ -819,10 +837,13 @@ oauth2Config: # highlight-end ``` -`subjectPath` is required; `namePath` and `emailPath` are optional. Each path -must be 1-256 characters long. If you set both `identityFromToken` and -`userInfo`, `identityFromToken` takes precedence and the userinfo HTTP call is -skipped. +`subjectPath` is required; `namePath` and `emailPath` are optional. Omit +`namePath` and `emailPath` rather than setting them to empty strings. + +If you set both `identityFromToken` and `userInfo`, `identityFromToken` takes +precedence and the userinfo HTTP call is skipped. If `identityFromToken` is set +and extraction fails (path missing or unexpected type), authentication fails for +that login attempt. There is no fallback to `userInfo`. :::warning[Trust model]