From 87b425348850b3d2e5f0221071e93b0679ba1ecd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:05:33 +0000 Subject: [PATCH 1/7] Update stacklok/toolhive to v0.32.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 4780f275..282f009e 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -44,7 +44,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.31.0 + version: v0.32.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 2b7f4379d268238fee5400915307e7973e4460ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Jun 2026 18:09:29 +0000 Subject: [PATCH 2/7] Refresh reference assets for toolhive v0.32.0 --- docs/toolhive/reference/cli/thv_proxy.md | 1 + docs/toolhive/reference/cli/thv_run.md | 1 + static/api-specs/toolhive-api.yaml | 18 ++ .../mcpexternalauthconfigs.schema.json | 9 +- .../virtualmcpservers.schema.json | 262 +++++++++++++++++- 5 files changed, 283 insertions(+), 8 deletions(-) diff --git a/docs/toolhive/reference/cli/thv_proxy.md b/docs/toolhive/reference/cli/thv_proxy.md index be2e8d92..6cbc09c2 100644 --- a/docs/toolhive/reference/cli/thv_proxy.md +++ b/docs/toolhive/reference/cli/thv_proxy.md @@ -97,6 +97,7 @@ thv proxy [flags] SERVER_NAME ### Options ``` + --allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com -h, --help help for proxy --host string Host for the HTTP proxy to listen on (IP or hostname) (default "127.0.0.1") --oidc-audience string Expected audience for the token diff --git a/docs/toolhive/reference/cli/thv_run.md b/docs/toolhive/reference/cli/thv_run.md index 4c0d1100..b3f5d7aa 100644 --- a/docs/toolhive/reference/cli/thv_run.md +++ b/docs/toolhive/reference/cli/thv_run.md @@ -112,6 +112,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] ``` --allow-docker-gateway Allow outbound connections to Docker gateway addresses (host.docker.internal, gateway.docker.internal, 172.17.0.1). Only applies when --isolate-network is set. These are blocked by default even when insecure_allow_all is enabled. + --allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com --audit-config string Path to the audit configuration file --authz-config string Path to the authorization configuration file --ca-cert string Path to a custom CA certificate file to use for container builds diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 63d3dd71..b0c70cfa 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -640,6 +640,12 @@ components: type: string type: array uniqueItems: false + insecure_allow_http: + description: |- + InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts. + Only set this for in-cluster Kubernetes deployments on a trusted network. + Production deployments reachable outside the cluster MUST use https://. + type: boolean issuer: description: |- Issuer is the issuer identifier for this authorization server. @@ -1309,6 +1315,18 @@ components: blocked by default in the egress proxy even when InsecureAllowAll is set. Only applicable to Docker deployments with network isolation enabled. type: boolean + allowed_origins: + description: |- + AllowedOrigins is the allowlist of values accepted on the HTTP Origin header, + used for DNS-rebinding protection per MCP 2025-11-25 §"Security Warning". + When empty and Host is loopback (127.0.0.1 / localhost / [::1]), a default + loopback-only allowlist is derived at middleware-wiring time. + When empty and Host is non-loopback, the middleware is disabled — operators + exposing the proxy publicly must configure an explicit allowlist. + items: + type: string + type: array + uniqueItems: false audit_config: $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_audit.Config' audit_config_path: diff --git a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json index d109cf30..6b2153fe 100644 --- a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json @@ -128,7 +128,7 @@ "description": "EmbeddedAuthServer configures an embedded OAuth2/OIDC authorization server\nOnly used when Type is \"embeddedAuthServer\"", "properties": { "authorizationEndpointBaseUrl": { - "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.", + "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts\nwhen insecureAllowHTTP is true) without query, fragment, or trailing slash.", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -195,8 +195,13 @@ "type": "array", "x-kubernetes-list-type": "atomic" }, + "insecureAllowHTTP": { + "default": false, + "description": "InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.\nOnly set this for in-cluster Kubernetes deployments where traffic between\npods traverses a trusted network (e.g. the in-cluster service mesh).\nProduction deployments reachable outside the cluster MUST use https://.\n\nOn VirtualMCPServer: when false (the default), http:// issuers for non-localhost\nhosts are rejected at reconcile time with an AuthServerConfigValidated=False condition.\n\nOn MCPServer and MCPRemoteProxy (via MCPExternalAuthConfig): this field is\nstructurally present but enforcement is deferred to pod startup via Config.Validate();\na misconfigured issuer will cause the pod to crash at startup rather than surface\nas an operator condition.", + "type": "boolean" + }, "issuer": { - "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash (per RFC 8414).", + "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts when\ninsecureAllowHTTP is true) without query, fragment, or trailing slash (per RFC 8414).", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json index b57e129e..eac2abc4 100644 --- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json +++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json @@ -19,7 +19,7 @@ "description": "AuthServerConfig configures an embedded OAuth authorization server.\nWhen set, the vMCP server acts as an OIDC issuer, drives users through\nupstream IDPs, and issues ToolHive JWTs. The embedded AS becomes the\nIncomingAuth OIDC provider — its issuer must match IncomingAuth.OIDCConfigRef\nso that tokens it issues are accepted by the vMCP's incoming auth middleware.\nWhen nil, IncomingAuth uses an external IDP and behavior is unchanged.", "properties": { "authorizationEndpointBaseUrl": { - "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.", + "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts\nwhen insecureAllowHTTP is true) without query, fragment, or trailing slash.", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -86,8 +86,13 @@ "type": "array", "x-kubernetes-list-type": "atomic" }, + "insecureAllowHTTP": { + "default": false, + "description": "InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.\nOnly set this for in-cluster Kubernetes deployments where traffic between\npods traverses a trusted network (e.g. the in-cluster service mesh).\nProduction deployments reachable outside the cluster MUST use https://.\n\nOn VirtualMCPServer: when false (the default), http:// issuers for non-localhost\nhosts are rejected at reconcile time with an AuthServerConfigValidated=False condition.\n\nOn MCPServer and MCPRemoteProxy (via MCPExternalAuthConfig): this field is\nstructurally present but enforcement is deferred to pod startup via Config.Validate();\na misconfigured issuer will cause the pod to crash at startup rather than surface\nas an operator condition.", + "type": "boolean" + }, "issuer": { - "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash (per RFC 8414).", + "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts when\ninsecureAllowHTTP is true) without query, fragment, or trailing slash (per RFC 8414).", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -1498,6 +1503,19 @@ "optimizer": { "description": "Optimizer configures the MCP optimizer for context optimization on large toolsets.\nWhen enabled, vMCP exposes only find_tool and call_tool operations to clients\ninstead of all backend tools directly. This reduces token usage by allowing\nLLMs to discover relevant tools on demand rather than receiving all tool definitions.", "properties": { + "embeddingModel": { + "description": "EmbeddingModel is the model name requested from the embedding service\n(e.g. \"text-embedding-3-small\"). Required when EmbeddingProvider is\n\"openai\". Ignored for the \"tei\" provider, where the model is fixed by the\nrunning TEI container.\n\nThe API key for an OpenAI-compatible service is not configured here: it is\nread from the OPENAI_API_KEY environment variable so the secret never\nlands in a CRD spec or ConfigMap. An empty key omits the Authorization\nheader, which supports keyless in-cluster gateways.", + "type": "string" + }, + "embeddingProvider": { + "default": "tei", + "description": "EmbeddingProvider selects the wire protocol used to talk to the embedding\nservice. \"tei\" speaks the HuggingFace Text Embeddings Inference API;\n\"openai\" speaks the OpenAI-compatible /embeddings API, which lets the\noptimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.\nDefaults to \"tei\" when empty.\n\nThe \"openai\" provider reads EmbeddingService directly and cannot be combined\nwith EmbeddingServerRef, which provisions a managed TEI server; the operator\nrejects that combination at admission.", + "enum": [ + "tei", + "openai" + ], + "type": "string" + }, "embeddingService": { "description": "EmbeddingService is the full base URL of the embedding service endpoint\n(e.g., http://my-embedding.default.svc.cluster.local:8080) for semantic\ntool discovery.\n\nIn a Kubernetes environment, it is more convenient to use the\nVirtualMCPServerSpec.EmbeddingServerRef field instead of setting this\ndirectly. EmbeddingServerRef references an EmbeddingServer CRD by name,\nand the operator automatically resolves the referenced resource's\nStatus.URL to populate this field. This provides managed lifecycle\n(the operator watches the EmbeddingServer for readiness and URL changes)\nand avoids hardcoding service URLs in the config. If both\nEmbeddingServerRef and this field are set, EmbeddingServerRef takes\nprecedence and this value is overridden with a warning.", "type": "string" @@ -1623,6 +1641,52 @@ ], "type": "object" }, + "obo": { + "description": "OBO contains configuration for on-behalf-of (OBO) auth strategy.\nUsed when Type = \"obo\". The default upstream build returns ErrEnterpriseRequired;\nan out-of-tree build registers a real strategy via auth.RegisterOBOStrategy.", + "properties": { + "audience": { + "description": "Audience is the target audience (resource URI) for the exchanged token.", + "type": "string" + }, + "cacheSkewSeconds": { + "description": "CacheSkewSeconds is the number of seconds to subtract from a cached token's\nexpiry when deciding whether to refresh it. Defaults to zero (no skew).\nThe operator CRD stores this as CacheSkew *metav1.Duration and converts it\nto an integer-seconds value for the vMCP runtime contract.", + "format": "int32", + "type": "integer" + }, + "clientId": { + "description": "ClientID is the OAuth client ID for the OBO request.", + "type": "string" + }, + "clientSecret": { + "description": "ClientSecret is the OAuth client secret (use ClientSecretEnv for security).", + "type": "string" + }, + "clientSecretEnv": { + "description": "ClientSecretEnv is the environment variable name containing the client secret.\nThe value will be resolved at runtime from this environment variable.", + "type": "string" + }, + "scopes": { + "description": "Scopes are the requested scopes for the exchanged token.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectTokenProviderName": { + "description": "SubjectTokenProviderName selects which upstream provider's token to use as the\nsubject (assertion) token for the OBO exchange. When set, the token is looked\nup from Identity.UpstreamTokens[SubjectTokenProviderName]; when omitted, the\ninbound end-user token (Identity.Token) is used directly.\nMatches the operator CRD's SubjectTokenProviderName field; the enterprise OBO\nconverter maps both to the runtime contract without renaming.", + "type": "string" + }, + "tokenUrl": { + "description": "TokenURL is the Entra token endpoint URL for the OBO exchange.", + "type": "string" + } + }, + "required": [ + "tokenUrl" + ], + "type": "object" + }, "tokenExchange": { "description": "TokenExchange contains configuration for token exchange auth strategy.\nUsed when Type = \"token_exchange\".", "properties": { @@ -1647,7 +1711,8 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "x-kubernetes-list-type": "atomic" }, "subjectProviderName": { "description": "SubjectProviderName selects which upstream provider's token to use as the\nsubject token. When set, the token is looked up from Identity.UpstreamTokens\ninstead of using Identity.Token.\nWhen left empty and an embedded authorization server is configured, the system\nautomatically populates this field with the first configured upstream provider name.\nSet it explicitly to override that default or to select a specific provider when\nmultiple upstreams are configured.", @@ -1668,7 +1733,7 @@ "type": "object" }, "type": { - "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\"", + "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\", \"xaa\"", "type": "string" }, "upstreamInject": { @@ -1683,6 +1748,73 @@ "providerName" ], "type": "object" + }, + "xaa": { + "description": "XAA contains configuration for XAA (Cross-Application Access) auth strategy.\nUsed when Type = \"xaa\".", + "properties": { + "idpClientId": { + "description": "IDPClientID is the OAuth client ID at the IdP for Step A.", + "type": "string" + }, + "idpClientSecret": { + "description": "IDPClientSecret is the client secret at the IdP for Step A.", + "type": "string" + }, + "idpClientSecretEnv": { + "description": "IDPClientSecretEnv is the env var containing the IdP client secret.", + "type": "string" + }, + "idpTokenUrl": { + "description": "IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange).", + "type": "string" + }, + "insecureTargetTokenUrl": { + "description": "InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.\nWARNING: this is insecure and must only be set for in-cluster or\ndevelopment/testing endpoints — never in production.", + "type": "boolean" + }, + "scopes": { + "description": "Scopes are the requested scopes for Steps A and B.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectProviderName": { + "description": "SubjectProviderName selects which upstream provider's ID token to use.\nAuto-populated when embedded AS is active.", + "type": "string" + }, + "targetAudience": { + "description": "TargetAudience is the resource AS URL for the ID-JAG audience claim (required).", + "type": "string" + }, + "targetClientId": { + "description": "TargetClientID is the OAuth client ID at the target AS for Step B.", + "type": "string" + }, + "targetClientSecret": { + "description": "TargetClientSecret is the client secret at the target AS for Step B.", + "type": "string" + }, + "targetClientSecretEnv": { + "description": "TargetClientSecretEnv is the env var containing the target AS client secret.", + "type": "string" + }, + "targetResource": { + "description": "TargetResource is the RFC 8707 resource indicator sent as the `resource`\nparameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It\nidentifies the target resource server — not the access-token audience, which\nis governed by TargetAudience. For MCP backends, set to the MCP server URL.", + "type": "string" + }, + "targetTokenUrl": { + "description": "TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant).", + "type": "string" + } + }, + "required": [ + "idpTokenUrl", + "targetAudience", + "targetTokenUrl" + ], + "type": "object" } }, "required": [ @@ -1785,6 +1917,52 @@ ], "type": "object" }, + "obo": { + "description": "OBO contains configuration for on-behalf-of (OBO) auth strategy.\nUsed when Type = \"obo\". The default upstream build returns ErrEnterpriseRequired;\nan out-of-tree build registers a real strategy via auth.RegisterOBOStrategy.", + "properties": { + "audience": { + "description": "Audience is the target audience (resource URI) for the exchanged token.", + "type": "string" + }, + "cacheSkewSeconds": { + "description": "CacheSkewSeconds is the number of seconds to subtract from a cached token's\nexpiry when deciding whether to refresh it. Defaults to zero (no skew).\nThe operator CRD stores this as CacheSkew *metav1.Duration and converts it\nto an integer-seconds value for the vMCP runtime contract.", + "format": "int32", + "type": "integer" + }, + "clientId": { + "description": "ClientID is the OAuth client ID for the OBO request.", + "type": "string" + }, + "clientSecret": { + "description": "ClientSecret is the OAuth client secret (use ClientSecretEnv for security).", + "type": "string" + }, + "clientSecretEnv": { + "description": "ClientSecretEnv is the environment variable name containing the client secret.\nThe value will be resolved at runtime from this environment variable.", + "type": "string" + }, + "scopes": { + "description": "Scopes are the requested scopes for the exchanged token.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectTokenProviderName": { + "description": "SubjectTokenProviderName selects which upstream provider's token to use as the\nsubject (assertion) token for the OBO exchange. When set, the token is looked\nup from Identity.UpstreamTokens[SubjectTokenProviderName]; when omitted, the\ninbound end-user token (Identity.Token) is used directly.\nMatches the operator CRD's SubjectTokenProviderName field; the enterprise OBO\nconverter maps both to the runtime contract without renaming.", + "type": "string" + }, + "tokenUrl": { + "description": "TokenURL is the Entra token endpoint URL for the OBO exchange.", + "type": "string" + } + }, + "required": [ + "tokenUrl" + ], + "type": "object" + }, "tokenExchange": { "description": "TokenExchange contains configuration for token exchange auth strategy.\nUsed when Type = \"token_exchange\".", "properties": { @@ -1809,7 +1987,8 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "x-kubernetes-list-type": "atomic" }, "subjectProviderName": { "description": "SubjectProviderName selects which upstream provider's token to use as the\nsubject token. When set, the token is looked up from Identity.UpstreamTokens\ninstead of using Identity.Token.\nWhen left empty and an embedded authorization server is configured, the system\nautomatically populates this field with the first configured upstream provider name.\nSet it explicitly to override that default or to select a specific provider when\nmultiple upstreams are configured.", @@ -1830,7 +2009,7 @@ "type": "object" }, "type": { - "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\"", + "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\", \"xaa\"", "type": "string" }, "upstreamInject": { @@ -1845,6 +2024,73 @@ "providerName" ], "type": "object" + }, + "xaa": { + "description": "XAA contains configuration for XAA (Cross-Application Access) auth strategy.\nUsed when Type = \"xaa\".", + "properties": { + "idpClientId": { + "description": "IDPClientID is the OAuth client ID at the IdP for Step A.", + "type": "string" + }, + "idpClientSecret": { + "description": "IDPClientSecret is the client secret at the IdP for Step A.", + "type": "string" + }, + "idpClientSecretEnv": { + "description": "IDPClientSecretEnv is the env var containing the IdP client secret.", + "type": "string" + }, + "idpTokenUrl": { + "description": "IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange).", + "type": "string" + }, + "insecureTargetTokenUrl": { + "description": "InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.\nWARNING: this is insecure and must only be set for in-cluster or\ndevelopment/testing endpoints — never in production.", + "type": "boolean" + }, + "scopes": { + "description": "Scopes are the requested scopes for Steps A and B.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectProviderName": { + "description": "SubjectProviderName selects which upstream provider's ID token to use.\nAuto-populated when embedded AS is active.", + "type": "string" + }, + "targetAudience": { + "description": "TargetAudience is the resource AS URL for the ID-JAG audience claim (required).", + "type": "string" + }, + "targetClientId": { + "description": "TargetClientID is the OAuth client ID at the target AS for Step B.", + "type": "string" + }, + "targetClientSecret": { + "description": "TargetClientSecret is the client secret at the target AS for Step B.", + "type": "string" + }, + "targetClientSecretEnv": { + "description": "TargetClientSecretEnv is the env var containing the target AS client secret.", + "type": "string" + }, + "targetResource": { + "description": "TargetResource is the RFC 8707 resource indicator sent as the `resource`\nparameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It\nidentifies the target resource server — not the access-token audience, which\nis governed by TargetAudience. For MCP backends, set to the MCP server URL.", + "type": "string" + }, + "targetTokenUrl": { + "description": "TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant).", + "type": "string" + } + }, + "required": [ + "idpTokenUrl", + "targetAudience", + "targetTokenUrl" + ], + "type": "object" } }, "required": [ @@ -2522,6 +2768,10 @@ { "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')" + }, + { + "message": "embeddingServerRef provisions a managed TEI server and cannot be combined with optimizer.embeddingProvider 'openai'; openai mode uses embeddingService directly", + "rule": "!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer) && has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider == 'openai')" } ] }, From e2f7489a535cbc0f994fc89326a8924ca3a36bbf Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:23:14 +0000 Subject: [PATCH 3/7] Document toolhive v0.32.0 user-facing changes Cover the new --allowed-origins flag, XAA outgoing-auth strategy, OpenAI-compatible embedding provider, EmbeddedAuthServer insecureAllowHTTP, and MCPRemoteProxy OIDC CA bundle behavior. --- docs/toolhive/guides-cli/run-mcp-servers.mdx | 39 ++++++++ docs/toolhive/guides-k8s/remote-mcp-proxy.mdx | 12 +++ docs/toolhive/guides-vmcp/authentication.mdx | 96 +++++++++++++++++++ docs/toolhive/guides-vmcp/local-cli.mdx | 7 ++ docs/toolhive/guides-vmcp/optimizer.mdx | 82 +++++++++++++++- docs/toolhive/tutorials/mcp-optimizer.mdx | 7 +- 6 files changed, 238 insertions(+), 5 deletions(-) diff --git a/docs/toolhive/guides-cli/run-mcp-servers.mdx b/docs/toolhive/guides-cli/run-mcp-servers.mdx index 0ac0fd54..2fa24c1e 100644 --- a/docs/toolhive/guides-cli/run-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/run-mcp-servers.mdx @@ -349,6 +349,45 @@ thv run --transport streamable-http --target-port -- http Check your MCP server's documentation for the required transport and port configuration. +### Restrict browser Origin headers + +ToolHive validates the HTTP `Origin` header on inbound proxy requests to protect +browser-based clients against DNS-rebinding attacks, per the +[MCP 2025-11-25 specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#security-warning). +Requests without an `Origin` header (such as IDE clients, CLI bridges, and +SDK-based MCP clients) pass through unchanged; only browser cross-origin +requests are subject to the check. + +When ToolHive binds to a loopback address (`127.0.0.1`, `localhost`, or `[::1]`, +which is the default), it derives a loopback-only allowlist automatically and no +configuration is required. When you bind to a non-loopback address with `--host` +and a browser client connects from a different origin, you must pass +`--allowed-origins` to enable the check: + +```bash +thv run --transport streamable-http \ + --host 0.0.0.0 \ + --allowed-origins https://my-web-app.example.com \ + +``` + +The flag is repeatable and matches each value exactly on scheme, host, and port. +If you bind to a non-loopback address without `--allowed-origins`, ToolHive logs +a warning and disables the check; this leaves browser clients exposed and is not +recommended for public binds. + +The same `--allowed-origins` flag is also accepted by `thv proxy`. + +:::info[Legacy SSE transport CORS] + +As of v0.32.0, the legacy SSE transport no longer sends a wildcard +`Access-Control-Allow-Origin: *` header. Browser clients on a non-loopback +origin that used to rely on the wildcard must now be added to the +`--allowed-origins` allowlist, or migrated off SSE to the streamable-HTTP +transport. + +::: + ### Add a custom CA certificate In corporate environments with TLS inspection or custom certificate authorities, diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index d6cef3c4..56838daa 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -289,6 +289,18 @@ For production deployments: ::: +:::info[OIDC CA bundle is mounted into the proxy pod] + +When you set `caBundleRef` on an MCPOIDCConfig referenced by an MCPRemoteProxy, +the operator mounts the referenced ConfigMap into the proxy pod and uses it for +the proxy's OIDC HTTPS calls (issuer discovery and JWKS fetch). A +`CABundleRefValidated` condition reflects whether the bundle was resolved +successfully; if the ConfigMap is missing or the key contains no valid PEM +certificates, reconciliation fails before the pod starts rather than the pod +crashing later with an opaque TLS error. + +::: + ### Authorization policies Authorization policies are written in diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 8aa1fe17..d2d06466 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -388,6 +388,92 @@ spec: name: exchange-okta ``` +### Cross-application access (XAA) + +The `xaa` outgoing auth strategy implements +[Identity Assertion Authorization Grant](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/) +(ID-JAG), a two-step token exchange that lets a user's session at one identity +provider obtain an access token at a separate target authorization server +without prompting the user again. Use it when each backend is fronted by its own +authorization server and you cannot or do not want to federate them with your +primary IdP, but you do want a single user login at the vMCP to grant access to +all of them. + +The strategy performs both steps on every backend call: + +1. **Step A ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)):** + Exchange the user's upstream ID token at the primary IdP for an ID-JAG + assertion that names the target audience. +2. **Step B ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)):** + Present the ID-JAG to the target authorization server as a JWT bearer + assertion to obtain a backend-scoped access token. + +vMCP's `TokenCache` reuses the resulting access token across requests; the +strategy itself holds no local cache. + +Configure XAA on an `MCPExternalAuthConfig` and reference it from the vMCP's +`outgoingAuth.backends` map: + +```yaml title="MCPExternalAuthConfig resource" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPExternalAuthConfig +metadata: + name: xaa-analytics + namespace: toolhive-system +spec: + type: xaa + xaa: + # Step A: exchange the user's ID token at the IdP for an ID-JAG. + idpTokenUrl: https://idp.example.com/oauth2/token + idpClientId: + idpClientSecretEnv: IDP_CLIENT_SECRET + targetAudience: https://analytics.example.com + targetResource: https://analytics.example.com/mcp + + # Step B: present the ID-JAG at the target AS for an access token. + targetTokenUrl: https://as.analytics.example.com/oauth2/token + targetClientId: + targetClientSecretEnv: TARGET_CLIENT_SECRET + + scopes: + - mcp:read + # When the embedded auth server is active, this is auto-populated with the + # first configured upstream provider. Set explicitly to pick a specific one. + subjectProviderName: idp +``` + +Then wire the config into a backend in the `outgoingAuth` map: + +```yaml title="VirtualMCPServer resource" +spec: + outgoingAuth: + source: inline + backends: + backend-analytics: + type: externalAuthConfigRef + externalAuthConfigRef: + name: xaa-analytics +``` + +XAA requires an [embedded authorization server](#embedded-authorization-server) +so that vMCP holds an upstream ID token to use as the Step A subject. The +`subjectProviderName` field selects which upstream provider's ID token to use; +when omitted, the embedded auth server fills in the first configured upstream +provider at admission. + +`idpClientSecretEnv` and `targetClientSecretEnv` read the corresponding secrets +from environment variables on the vmcp container. Inject them from Kubernetes +Secrets via `podTemplateSpec` so the values never appear in the CRD spec or +ConfigMap. + +:::warning[Plain HTTP for the target token URL] + +XAA exposes an `insecureTargetTokenUrl: true` field that allows an `http://` +`targetTokenUrl`. Set it only for in-cluster development or test endpoints - +never for production traffic that leaves the cluster. + +::: + ## Embedded authorization server The embedded authorization server runs an OAuth authorization server within the @@ -513,6 +599,16 @@ for key generation steps. ::: +The issuer URL must use the `https://` scheme. The single exception is +`localhost`, which can use `http://` for local development. For in-cluster +deployments where traffic between the embedded auth server and other pods stays +on a trusted network (for example, an in-cluster service mesh), you can opt in +to an `http://` issuer on a non-localhost host by setting +`insecureAllowHTTP: true`. The VirtualMCPServer controller rejects this +combination at reconcile time with `AuthServerConfigValidated=False` if the flag +is unset, so misconfiguration surfaces on the resource rather than crashing the +pod at startup. Never set this for issuers reachable outside the cluster. + If the browser-facing authorization endpoint needs to be on a different host than the issuer (for example, behind an ingress that rewrites paths), set `authorizationEndpointBaseUrl` to override the `authorization_endpoint` in the diff --git a/docs/toolhive/guides-vmcp/local-cli.mdx b/docs/toolhive/guides-vmcp/local-cli.mdx index 05b3b50c..6d952e13 100644 --- a/docs/toolhive/guides-vmcp/local-cli.mdx +++ b/docs/toolhive/guides-vmcp/local-cli.mdx @@ -240,6 +240,13 @@ For Tier 2, ToolHive starts and stops a HuggingFace Text Embeddings Inference (TEI) container named `thv-embedding-` automatically. Customize the model and image with `--embedding-model` and `--embedding-image`. +For Tier 3, you can point at any HuggingFace TEI server or at an +OpenAI-compatible `/embeddings` endpoint (OpenAI, Azure OpenAI, or another +compatible gateway). Set `embeddingProvider: openai` and `embeddingModel` +alongside `embeddingService`, and supply the API key via the `OPENAI_API_KEY` +environment variable (omit it for keyless gateways). The default is `tei`, so +existing Tier 3 configs continue to work unchanged. + For the conceptual background and tuning parameters, see [Optimize tool discovery](./optimizer.mdx) and [Tool optimization](../concepts/tool-optimization.mdx). diff --git a/docs/toolhive/guides-vmcp/optimizer.mdx b/docs/toolhive/guides-vmcp/optimizer.mdx index dfd513e7..a20fe068 100644 --- a/docs/toolhive/guides-vmcp/optimizer.mdx +++ b/docs/toolhive/guides-vmcp/optimizer.mdx @@ -80,9 +80,14 @@ toolset. ## EmbeddingServer resource -The EmbeddingServer CRD manages the lifecycle of a TEI server. An empty -`spec: {}` uses all defaults. The two most important fields you can customize -are: +The EmbeddingServer CRD manages the lifecycle of a managed TEI server, which is +the default embedding backend. If you'd rather point the optimizer at an +external OpenAI-compatible embedding service instead, see +[Use an OpenAI-compatible embedding service](#use-an-openai-compatible-embedding-service) +below. + +An empty `spec: {}` uses all defaults. The two most important fields you can +customize are: - **`model`**: The Hugging Face embedding model to use. The default (`BAAI/bge-small-en-v1.5`) is the tested and recommended model. You can @@ -115,6 +120,77 @@ spec: ::: +## Use an OpenAI-compatible embedding service + +Instead of running a managed TEI EmbeddingServer, you can point the optimizer at +an external service that speaks the OpenAI `/embeddings` API - OpenAI itself, +Azure OpenAI, or another OpenAI-compatible gateway. Use this when you already +operate a centralized embedding service and don't want a second copy running per +vMCP, or when you need a hosted model. + +Set `embeddingProvider: openai` under `spec.config.optimizer` and configure +`embeddingService` and `embeddingModel` directly. Do **not** set +`embeddingServerRef`; the operator rejects combining the two at admission. + +```yaml title="VirtualMCPServer resource" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: VirtualMCPServer +metadata: + name: optimizer-vmcp + namespace: toolhive-system +spec: + groupRef: + name: my-group + config: + optimizer: + # highlight-start + embeddingProvider: openai + embeddingService: http://llm-gateway.default.svc.cluster.local:8080/v1 + embeddingModel: text-embedding-3-small + # highlight-end + embeddingServiceTimeout: 15s + incomingAuth: + type: anonymous +``` + +`embeddingService` is the base URL of the OpenAI-compatible endpoint; +`/embeddings` is appended automatically. `embeddingModel` is the model name +passed in each request and is required for the `openai` provider (the `tei` +provider ignores it, because the model is fixed by the TEI container). + +The API key for the embedding service is read from the `OPENAI_API_KEY` +environment variable on the vmcp container, never from the CRD spec or +ConfigMap. Inject it from a Secret via `podTemplateSpec`: + +```yaml title="VirtualMCPServer resource (excerpt)" +spec: + podTemplateSpec: + spec: + containers: + - name: vmcp + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: embedding-api-key + key: apiKey +``` + +Omit the env var entirely if your gateway is keyless (for example, an in-cluster +LLM gateway that authenticates by network position). An empty key omits the +`Authorization` header. + +:::warning[Inputs are not truncated] + +Unlike the TEI backend, the OpenAI API does not silently truncate over-long +inputs. A tool description that exceeds the model's context window causes the +request to fail with an error rather than being truncated. + +::: + +When `embeddingProvider` is omitted, the optimizer defaults to `tei` and your +existing TEI-based configuration continues to work unchanged. + ## Local mode (CLI) You can enable the optimizer directly from the `thv vmcp` CLI without a diff --git a/docs/toolhive/tutorials/mcp-optimizer.mdx b/docs/toolhive/tutorials/mcp-optimizer.mdx index a1fe2453..2129365a 100644 --- a/docs/toolhive/tutorials/mcp-optimizer.mdx +++ b/docs/toolhive/tutorials/mcp-optimizer.mdx @@ -181,8 +181,11 @@ Then apply the YAML above, which creates a new `fetch` server with the correct ## Step 2: Deploy an EmbeddingServer -The optimizer uses semantic search to find relevant tools. This requires an -EmbeddingServer, which runs a text embeddings inference (TEI) server. +The optimizer uses semantic search to find relevant tools, which means it needs +to talk to an embedding service. This tutorial deploys a managed EmbeddingServer +that runs a HuggingFace Text Embeddings Inference (TEI) container. If you'd +rather point at an existing OpenAI-compatible embedding service, see +[Use an OpenAI-compatible embedding service](../guides-vmcp/optimizer.mdx#use-an-openai-compatible-embedding-service). Create an EmbeddingServer with default settings. This deploys the `BAAI/bge-small-en-v1.5` model. If you are running on ARM64 nodes (for example, From b4768ba66c5849291cc75968d1581cc66e608c8e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:27:12 +0000 Subject: [PATCH 4/7] Smooth out two spaced-hyphen separators in v0.32.0 docs --- docs/toolhive/guides-vmcp/authentication.mdx | 2 +- docs/toolhive/guides-vmcp/optimizer.mdx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index d2d06466..154e29c3 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -469,7 +469,7 @@ ConfigMap. :::warning[Plain HTTP for the target token URL] XAA exposes an `insecureTargetTokenUrl: true` field that allows an `http://` -`targetTokenUrl`. Set it only for in-cluster development or test endpoints - +`targetTokenUrl`. Set it only for in-cluster development or test endpoints, never for production traffic that leaves the cluster. ::: diff --git a/docs/toolhive/guides-vmcp/optimizer.mdx b/docs/toolhive/guides-vmcp/optimizer.mdx index a20fe068..18d0e0da 100644 --- a/docs/toolhive/guides-vmcp/optimizer.mdx +++ b/docs/toolhive/guides-vmcp/optimizer.mdx @@ -123,10 +123,10 @@ spec: ## Use an OpenAI-compatible embedding service Instead of running a managed TEI EmbeddingServer, you can point the optimizer at -an external service that speaks the OpenAI `/embeddings` API - OpenAI itself, -Azure OpenAI, or another OpenAI-compatible gateway. Use this when you already -operate a centralized embedding service and don't want a second copy running per -vMCP, or when you need a hosted model. +an external service that speaks the OpenAI `/embeddings` API, such as OpenAI +itself, Azure OpenAI, or another OpenAI-compatible gateway. Use this when you +already operate a centralized embedding service and don't want a second copy +running per vMCP, or when you need a hosted model. Set `embeddingProvider: openai` under `spec.config.optimizer` and configure `embeddingService` and `embeddingModel` directly. Do **not** set From 458425ea8d21ce84649ad6f2372d840d9dc1f73d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:09:39 +0000 Subject: [PATCH 5/7] Drop OIDC CA bundle admonition in remote proxy docs The admonition described expected behavior of caBundleRef (now fixed upstream in a bugfix), reading like a new feature. The actionable guidance to set caBundleRef already lives in the Production security warning, so the callout was redundant. Co-authored-by: Dan Barr --- docs/toolhive/guides-k8s/remote-mcp-proxy.mdx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index 56838daa..d6cef3c4 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -289,18 +289,6 @@ For production deployments: ::: -:::info[OIDC CA bundle is mounted into the proxy pod] - -When you set `caBundleRef` on an MCPOIDCConfig referenced by an MCPRemoteProxy, -the operator mounts the referenced ConfigMap into the proxy pod and uses it for -the proxy's OIDC HTTPS calls (issuer discovery and JWKS fetch). A -`CABundleRefValidated` condition reflects whether the bundle was resolved -successfully; if the ConfigMap is missing or the key contains no valid PEM -certificates, reconciliation fails before the pod starts rather than the pod -crashing later with an opaque TLS error. - -::: - ### Authorization policies Authorization policies are written in From 1e3c7e1921b190c2b0e843b2da7c5c38113f9b30 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:21:05 -0400 Subject: [PATCH 6/7] Tighten Origin headers section in run-mcp-servers Merge redundant non-loopback explanations into one paragraph and fix an ambiguous loopback-address parenthetical. Co-Authored-By: Claude Sonnet 5 --- docs/toolhive/guides-cli/run-mcp-servers.mdx | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/toolhive/guides-cli/run-mcp-servers.mdx b/docs/toolhive/guides-cli/run-mcp-servers.mdx index 2fa24c1e..bb36a6b0 100644 --- a/docs/toolhive/guides-cli/run-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/run-mcp-servers.mdx @@ -358,11 +358,15 @@ Requests without an `Origin` header (such as IDE clients, CLI bridges, and SDK-based MCP clients) pass through unchanged; only browser cross-origin requests are subject to the check. -When ToolHive binds to a loopback address (`127.0.0.1`, `localhost`, or `[::1]`, -which is the default), it derives a loopback-only allowlist automatically and no -configuration is required. When you bind to a non-loopback address with `--host` -and a browser client connects from a different origin, you must pass -`--allowed-origins` to enable the check: +When ToolHive binds to a loopback address (the default is `127.0.0.1`, but +`localhost` and `[::1]` also count), it derives a loopback-only allowlist +automatically and no configuration is required. + +When you bind to a non-loopback address with `--host` and a browser client +connects from a different origin, pass `--allowed-origins` to enable the check. +The flag is repeatable, matches each value exactly on scheme, host, and port, +and is also accepted by `thv proxy`. Without it, ToolHive logs a warning and +disables the check entirely, which isn't recommended for public binds: ```bash thv run --transport streamable-http \ @@ -371,13 +375,6 @@ thv run --transport streamable-http \ ``` -The flag is repeatable and matches each value exactly on scheme, host, and port. -If you bind to a non-loopback address without `--allowed-origins`, ToolHive logs -a warning and disables the check; this leaves browser clients exposed and is not -recommended for public binds. - -The same `--allowed-origins` flag is also accepted by `thv proxy`. - :::info[Legacy SSE transport CORS] As of v0.32.0, the legacy SSE transport no longer sends a wildcard From daa05d72a59f6393411334c86b9dd2627e1a76e6 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:20:06 -0400 Subject: [PATCH 7/7] Drop unsupported XAA example, fix optimizer admonition XAA isn't wired into the Kubernetes operator: the converter discards spec.config.outgoingAuth, and MCPExternalAuthConfig's type enum doesn't include xaa, so the CRD example in authentication.mdx can't work today. Remove it rather than document a feature that silently falls back to unauthenticated. Also scope the "EmbeddingServer is always required" admonition in optimizer.mdx to the default tei provider; it doesn't apply when embeddingService is set directly, e.g. via the OpenAI-compatible provider added in this release. Co-Authored-By: Claude Sonnet 5 --- docs/toolhive/guides-vmcp/authentication.mdx | 86 -------------------- docs/toolhive/guides-vmcp/optimizer.mdx | 27 +++--- 2 files changed, 17 insertions(+), 96 deletions(-) diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 154e29c3..27aa49e2 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -388,92 +388,6 @@ spec: name: exchange-okta ``` -### Cross-application access (XAA) - -The `xaa` outgoing auth strategy implements -[Identity Assertion Authorization Grant](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/) -(ID-JAG), a two-step token exchange that lets a user's session at one identity -provider obtain an access token at a separate target authorization server -without prompting the user again. Use it when each backend is fronted by its own -authorization server and you cannot or do not want to federate them with your -primary IdP, but you do want a single user login at the vMCP to grant access to -all of them. - -The strategy performs both steps on every backend call: - -1. **Step A ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)):** - Exchange the user's upstream ID token at the primary IdP for an ID-JAG - assertion that names the target audience. -2. **Step B ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)):** - Present the ID-JAG to the target authorization server as a JWT bearer - assertion to obtain a backend-scoped access token. - -vMCP's `TokenCache` reuses the resulting access token across requests; the -strategy itself holds no local cache. - -Configure XAA on an `MCPExternalAuthConfig` and reference it from the vMCP's -`outgoingAuth.backends` map: - -```yaml title="MCPExternalAuthConfig resource" -apiVersion: toolhive.stacklok.dev/v1beta1 -kind: MCPExternalAuthConfig -metadata: - name: xaa-analytics - namespace: toolhive-system -spec: - type: xaa - xaa: - # Step A: exchange the user's ID token at the IdP for an ID-JAG. - idpTokenUrl: https://idp.example.com/oauth2/token - idpClientId: - idpClientSecretEnv: IDP_CLIENT_SECRET - targetAudience: https://analytics.example.com - targetResource: https://analytics.example.com/mcp - - # Step B: present the ID-JAG at the target AS for an access token. - targetTokenUrl: https://as.analytics.example.com/oauth2/token - targetClientId: - targetClientSecretEnv: TARGET_CLIENT_SECRET - - scopes: - - mcp:read - # When the embedded auth server is active, this is auto-populated with the - # first configured upstream provider. Set explicitly to pick a specific one. - subjectProviderName: idp -``` - -Then wire the config into a backend in the `outgoingAuth` map: - -```yaml title="VirtualMCPServer resource" -spec: - outgoingAuth: - source: inline - backends: - backend-analytics: - type: externalAuthConfigRef - externalAuthConfigRef: - name: xaa-analytics -``` - -XAA requires an [embedded authorization server](#embedded-authorization-server) -so that vMCP holds an upstream ID token to use as the Step A subject. The -`subjectProviderName` field selects which upstream provider's ID token to use; -when omitted, the embedded auth server fills in the first configured upstream -provider at admission. - -`idpClientSecretEnv` and `targetClientSecretEnv` read the corresponding secrets -from environment variables on the vmcp container. Inject them from Kubernetes -Secrets via `podTemplateSpec` so the values never appear in the CRD spec or -ConfigMap. - -:::warning[Plain HTTP for the target token URL] - -XAA exposes an `insecureTargetTokenUrl: true` field that allows an `http://` -`targetTokenUrl`. Set it only for in-cluster development or test endpoints, -never for production traffic that leaves the cluster. - -::: - ## Embedded authorization server The embedded authorization server runs an OAuth authorization server within the diff --git a/docs/toolhive/guides-vmcp/optimizer.mdx b/docs/toolhive/guides-vmcp/optimizer.mdx index 18d0e0da..4cfbcd61 100644 --- a/docs/toolhive/guides-vmcp/optimizer.mdx +++ b/docs/toolhive/guides-vmcp/optimizer.mdx @@ -332,16 +332,23 @@ spec: exclude={['embeddingService']} /> -:::info[Kubernetes: EmbeddingServer is always required] - -When using the Kubernetes operator, even if you set `hybridSearchSemanticRatio` -to `"0.0"` (all keyword search), the optimizer still requires a configured -`EmbeddingServer`. The EmbeddingServer won't be used at runtime when the -semantic ratio is `0.0`, but the configuration must be present due to how the -operator wires the resources internally. - -This restriction does not apply to local CLI mode. `thv vmcp serve --optimizer` -runs keyword-only search with no EmbeddingServer and no container. +:::info[Kubernetes: EmbeddingServer is required for the default TEI provider] + +When using the Kubernetes operator with the default `tei` embedding provider, +even if you set `hybridSearchSemanticRatio` to `"0.0"` (all keyword search), the +optimizer still requires a configured `EmbeddingServer`. The EmbeddingServer +won't be used at runtime when the semantic ratio is `0.0`, but the configuration +must be present due to how the operator wires the resources internally. + +This restriction doesn't apply when you set `optimizer.embeddingService` +directly, such as with the +[OpenAI-compatible provider](#use-an-openai-compatible-embedding-service); the +operator only requires `embeddingServerRef` when no manual embedding service is +configured. + +This restriction also does not apply to local CLI mode. +`thv vmcp serve --optimizer` runs keyword-only search with no EmbeddingServer +and no container. :::