diff --git a/cmd/thv/app/proxy.go b/cmd/thv/app/proxy.go index 633c64b42..c2694fd3f 100644 --- a/cmd/thv/app/proxy.go +++ b/cmd/thv/app/proxy.go @@ -16,6 +16,7 @@ import ( "golang.org/x/oauth2" "github.com/stacklok/toolhive/pkg/auth" + "github.com/stacklok/toolhive/pkg/auth/discovery" "github.com/stacklok/toolhive/pkg/auth/oauth" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" @@ -113,15 +114,6 @@ var ( remoteAuthTokenURL string ) -// Default timeout constants -const ( - defaultOAuthTimeout = 5 * time.Minute - defaultHTTPTimeout = 30 * time.Second - defaultAuthDetectTimeout = 10 * time.Second - maxRetryAttempts = 3 - retryBaseDelay = 2 * time.Second -) - // Environment variable names const ( // #nosec G101 - this is an environment variable name, not a credential @@ -145,7 +137,7 @@ func init() { "Explicit resource URL for OAuth discovery endpoint (RFC 9728)") // Add remote server authentication flags - proxyCmd.Flags().BoolVar(&enableRemoteAuth, "remote-auth", false, "Enable OAuth authentication to remote MCP server") + proxyCmd.Flags().BoolVar(&enableRemoteAuth, "remote-auth", false, "Enable OAuth/OIDC authentication to remote MCP server") proxyCmd.Flags().StringVar(&remoteAuthIssuer, "remote-auth-issuer", "", "OAuth/OIDC issuer URL for remote server authentication (e.g., https://accounts.google.com)") proxyCmd.Flags().StringVar(&remoteAuthClientID, "remote-auth-client-id", "", @@ -287,196 +279,6 @@ func proxyCmdFunc(cmd *cobra.Command, args []string) error { return proxy.Stop(shutdownCtx) } -// AuthInfo contains authentication information extracted from WWW-Authenticate header -type AuthInfo struct { - Realm string - Type string -} - -// detectAuthenticationFromServer attempts to detect authentication requirements from the target server -func detectAuthenticationFromServer(ctx context.Context, targetURI string) (*AuthInfo, error) { - // Create a context with timeout for auth detection - detectCtx, cancel := context.WithTimeout(ctx, defaultAuthDetectTimeout) - defer cancel() - - // Make a test request to the target server to see if it returns WWW-Authenticate - client := &http.Client{ - Timeout: defaultAuthDetectTimeout, - Transport: &http.Transport{ - TLSHandshakeTimeout: defaultHTTPTimeout / 3, - ResponseHeaderTimeout: defaultHTTPTimeout / 3, - }, - } - - req, err := http.NewRequestWithContext(detectCtx, http.MethodGet, targetURI, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Check if we got a 401 Unauthorized with WWW-Authenticate header - if resp.StatusCode == http.StatusUnauthorized { - wwwAuth := resp.Header.Get("WWW-Authenticate") - if wwwAuth != "" { - return parseWWWAuthenticate(wwwAuth) - } - } - - return nil, nil -} - -// parseWWWAuthenticate parses the WWW-Authenticate header to extract realm and type -// Supports multiple authentication schemes and complex header formats -func parseWWWAuthenticate(header string) (*AuthInfo, error) { - // Trim whitespace and handle empty headers - header = strings.TrimSpace(header) - if header == "" { - return nil, fmt.Errorf("empty WWW-Authenticate header") - } - - // Split by comma to handle multiple authentication schemes - schemes := strings.Split(header, ",") - - for _, scheme := range schemes { - scheme = strings.TrimSpace(scheme) - - // Check for Bearer authentication - if strings.HasPrefix(scheme, "Bearer") { - authInfo := &AuthInfo{Type: "Bearer"} - - // Extract parameters after "Bearer" - params := strings.TrimSpace(strings.TrimPrefix(scheme, "Bearer")) - if params == "" { - // Simple "Bearer" without parameters - return authInfo, nil - } - - // Parse parameters (realm, scope, etc.) - realm := extractParameter(params, "realm") - if realm != "" { - authInfo.Realm = realm - } - - return authInfo, nil - } - - // Check for other authentication types (Basic, Digest, etc.) - if strings.HasPrefix(scheme, "Basic") { - return &AuthInfo{Type: "Basic"}, nil - } - - if strings.HasPrefix(scheme, "Digest") { - authInfo := &AuthInfo{Type: "Digest"} - realm := extractParameter(scheme, "realm") - if realm != "" { - authInfo.Realm = realm - } - return authInfo, nil - } - } - - return nil, fmt.Errorf("no supported authentication type found in header: %s", header) -} - -// extractParameter extracts a parameter value from an authentication header -func extractParameter(params, paramName string) string { - // Look for paramName=value or paramName="value" - parts := strings.Split(params, ",") - for _, part := range parts { - part = strings.TrimSpace(part) - if strings.HasPrefix(part, paramName+"=") { - value := strings.TrimPrefix(part, paramName+"=") - // Remove quotes if present - value = strings.Trim(value, `"`) - return value - } - } - return "" -} - -// performOAuthFlow performs the OAuth authentication flow -func performOAuthFlow(ctx context.Context, issuer, clientID, clientSecret string, - scopes []string) (*oauth2.TokenSource, *oauth.Config, error) { - logger.Info("Starting OAuth authentication flow...") - - var oauthConfig *oauth.Config - var err error - - // Check if we have manual OAuth endpoints configured - if remoteAuthAuthorizeURL != "" && remoteAuthTokenURL != "" { - logger.Info("Using manual OAuth configuration") - oauthConfig, err = oauth.CreateOAuthConfigManual( - clientID, - clientSecret, - remoteAuthAuthorizeURL, - remoteAuthTokenURL, - scopes, - true, // Enable PKCE by default for security - remoteAuthCallbackPort, - ) - } else { - // Fall back to OIDC discovery - logger.Info("Using OIDC discovery") - oauthConfig, err = oauth.CreateOAuthConfigFromOIDC( - ctx, - issuer, - clientID, - clientSecret, - scopes, - true, // Enable PKCE by default for security - remoteAuthCallbackPort, - ) - } - if err != nil { - return nil, nil, fmt.Errorf("failed to create OAuth config: %w", err) - } - - // Create OAuth flow - flow, err := oauth.NewFlow(oauthConfig) - if err != nil { - return nil, nil, fmt.Errorf("failed to create OAuth flow: %w", err) - } - - // Create a context with timeout for the OAuth flow - // Use the configured timeout, defaulting to the constant if not set - oauthTimeout := remoteAuthTimeout - if oauthTimeout <= 0 { - oauthTimeout = defaultOAuthTimeout - } - - oauthCtx, cancel := context.WithTimeout(ctx, oauthTimeout) - defer cancel() - - // Start OAuth flow - tokenResult, err := flow.Start(oauthCtx, remoteAuthSkipBrowser) - if err != nil { - if oauthCtx.Err() == context.DeadlineExceeded { - return nil, nil, fmt.Errorf("OAuth flow timed out after %v - user did not complete authentication", oauthTimeout) - } - return nil, nil, fmt.Errorf("OAuth flow failed: %w", err) - } - - logger.Info("OAuth authentication successful") - - // Log token info (without exposing the actual token) - if tokenResult.Claims != nil { - if sub, ok := tokenResult.Claims["sub"].(string); ok { - logger.Infof("Authenticated as subject: %s", sub) - } - if email, ok := tokenResult.Claims["email"].(string); ok { - logger.Infof("Authenticated email: %s", email) - } - } - - source := flow.TokenSource() - return &source, oauthConfig, nil -} - // shouldDetectAuth determines if we should try to detect authentication requirements func shouldDetectAuth() bool { // Only try to detect auth if OAuth client ID is provided @@ -511,11 +313,27 @@ func handleOutgoingAuthentication(ctx context.Context) (*oauth2.TokenSource, *oa return nil, nil, fmt.Errorf("cannot specify both OIDC issuer and manual OAuth endpoints - choose one approach") } - return performOAuthFlow(ctx, remoteAuthIssuer, remoteAuthClientID, clientSecret, remoteAuthScopes) + flowConfig := &discovery.OAuthFlowConfig{ + ClientID: remoteAuthClientID, + ClientSecret: clientSecret, + AuthorizeURL: remoteAuthAuthorizeURL, + TokenURL: remoteAuthTokenURL, + Scopes: remoteAuthScopes, + CallbackPort: remoteAuthCallbackPort, + Timeout: remoteAuthTimeout, + SkipBrowser: remoteAuthSkipBrowser, + } + + result, err := discovery.PerformOAuthFlow(ctx, remoteAuthIssuer, flowConfig) + if err != nil { + return nil, nil, err + } + + return result.TokenSource, result.Config, nil } // Try to detect authentication requirements from WWW-Authenticate header - authInfo, err := detectAuthenticationFromServer(ctx, proxyTargetURI) + authInfo, err := discovery.DetectAuthenticationFromServer(ctx, proxyTargetURI, nil) if err != nil { logger.Debugf("Could not detect authentication from server: %v", err) return nil, nil, nil // Not an error, just no auth detected @@ -529,7 +347,23 @@ func handleOutgoingAuthentication(ctx context.Context) (*oauth2.TokenSource, *oa } // Perform OAuth flow with discovered configuration - return performOAuthFlow(ctx, authInfo.Realm, remoteAuthClientID, clientSecret, remoteAuthScopes) + flowConfig := &discovery.OAuthFlowConfig{ + ClientID: remoteAuthClientID, + ClientSecret: clientSecret, + AuthorizeURL: remoteAuthAuthorizeURL, + TokenURL: remoteAuthTokenURL, + Scopes: remoteAuthScopes, + CallbackPort: remoteAuthCallbackPort, + Timeout: remoteAuthTimeout, + SkipBrowser: remoteAuthSkipBrowser, + } + + result, err := discovery.PerformOAuthFlow(ctx, authInfo.Realm, flowConfig) + if err != nil { + return nil, nil, err + } + + return result.TokenSource, result.Config, nil } return nil, nil, nil // No authentication required diff --git a/cmd/thv/app/proxy_tunnel.go b/cmd/thv/app/proxy_tunnel.go index 19abc9c40..9c83a32ec 100644 --- a/cmd/thv/app/proxy_tunnel.go +++ b/cmd/thv/app/proxy_tunnel.go @@ -6,12 +6,12 @@ import ( "fmt" "net/url" "os/signal" - "strings" "syscall" "github.com/spf13/cobra" "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/transport/types" "github.com/stacklok/toolhive/pkg/workloads" ) @@ -126,11 +126,16 @@ func resolveTarget(ctx context.Context, target string) (string, error) { } func looksLikeURL(s string) bool { + // Parse the URL once + u, err := url.Parse(s) + if err != nil { + return false + } + // Fast-path for common schemes - if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { + if u.Scheme == networking.HttpScheme || u.Scheme == networking.HttpsScheme { return true } - // Fallback parse check - u, err := url.Parse(s) - return err == nil && u.Scheme != "" && u.Host != "" + // Fallback check for other schemes + return u.Scheme != "" && u.Host != "" } diff --git a/cmd/thv/app/registry.go b/cmd/thv/app/registry.go index 20e55370a..ebf34297f 100644 --- a/cmd/thv/app/registry.go +++ b/cmd/thv/app/registry.go @@ -159,12 +159,18 @@ func printTextServers(servers []registry.ServerMetadata) { } } +// ServerType constants +const ( + ServerTypeRemote = "remote" + ServerTypeContainer = "container" +) + // getServerType returns the type of server (container or remote) func getServerType(server registry.ServerMetadata) string { if server.IsRemote() { - return "remote" + return ServerTypeRemote } - return "container" + return ServerTypeContainer } // printTextServerInfo prints detailed information about a server in text format diff --git a/cmd/thv/app/run.go b/cmd/thv/app/run.go index 9fab89e51..51c7908c1 100644 --- a/cmd/thv/app/run.go +++ b/cmd/thv/app/run.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "net/url" "os" "os/signal" "syscall" @@ -26,7 +27,7 @@ var runCmd = &cobra.Command{ Short: "Run an MCP server", Long: `Run an MCP server with the specified name, image, or protocol scheme. -ToolHive supports four ways to run an MCP server: +ToolHive supports five ways to run an MCP server: 1. From the registry: @@ -59,6 +60,14 @@ ToolHive supports four ways to run an MCP server: Runs an MCP server using a previously exported configuration file. +5. Remote MCP server: + + $ thv run --remote [--name ] + + Runs a remote MCP server as a workload, proxying requests to the specified URL. + This allows remote MCP servers to be managed like local workloads with full + support for client configuration, tool filtering, import/export, etc. + The container will be started with the specified transport mode and permission profile. Additional configuration can be provided via flags.`, Args: func(cmd *cobra.Command, args []string) error { @@ -66,6 +75,10 @@ permission profile. Additional configuration can be provided via flags.`, if runFlags.FromConfig != "" { return nil } + // If --remote is provided, no args are required + if runFlags.RemoteURL != "" { + return nil + } // Otherwise, require at least 1 argument return cobra.MinimumNArgs(1)(cmd, args) }, @@ -124,12 +137,22 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { // Get the name of the MCP server to run. // This may be a server name from the registry, a container image, or a protocol scheme. - // When using --from-config, no args are required + // When using --from-config or --remote, no args are required var serverOrImage string if len(args) > 0 { serverOrImage = args[0] } + // If --remote is provided but no name is given, generate a name from the URL + if runFlags.RemoteURL != "" && runFlags.Name == "" { + // Extract a name from the remote URL + name, err := deriveRemoteName() + if err != nil { + return err + } + runFlags.Name = name + } + // Process command arguments using os.Args to find everything after -- cmdArgs := parseCommandArguments(os.Args) @@ -182,6 +205,19 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { return workloadManager.RunWorkloadDetached(ctx, runnerConfig) } +func deriveRemoteName() (string, error) { + parsedURL, err := url.Parse(runFlags.RemoteURL) + if err != nil { + return "", fmt.Errorf("invalid remote URL: %v", err) + } + // Use the hostname as the base name + hostname := parsedURL.Hostname() + if hostname == "" { + hostname = "remote" + } + return fmt.Sprintf("%s-remote", hostname), nil +} + func runForeground(ctx context.Context, workloadManager workloads.Manager, runnerConfig *runner.RunConfig) error { ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/cmd/thv/app/run_flags.go b/cmd/thv/app/run_flags.go index 9641023aa..86e9feaea 100644 --- a/cmd/thv/app/run_flags.go +++ b/cmd/thv/app/run_flags.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "strings" + "time" "github.com/spf13/cobra" "github.com/stacklok/toolhive/pkg/auth" + "github.com/stacklok/toolhive/pkg/auth/discovery" "github.com/stacklok/toolhive/pkg/authz" cfg "github.com/stacklok/toolhive/pkg/config" "github.com/stacklok/toolhive/pkg/container" @@ -24,6 +26,10 @@ import ( "github.com/stacklok/toolhive/pkg/transport/types" ) +const ( + defaultTransportType = "streamable-http" +) + // RunFlags holds the configuration for running MCP servers type RunFlags struct { // Transport and proxy settings @@ -42,6 +48,9 @@ type RunFlags struct { Volumes []string Secrets []string + // Remote MCP server support + RemoteURL string + // Security and audit AuthzConfig string AuditConfig string @@ -87,6 +96,23 @@ type RunFlags struct { // Ignore functionality IgnoreGlobally bool PrintOverlays bool + + // Remote authentication + EnableRemoteAuth bool + RemoteAuthClientID string + RemoteAuthClientSecret string + RemoteAuthClientSecretFile string + RemoteAuthScopes []string + RemoteAuthSkipBrowser bool + RemoteAuthTimeout time.Duration + RemoteAuthCallbackPort int + RemoteAuthIssuer string + RemoteAuthAuthorizeURL string + RemoteAuthTokenURL string + OAuthParams map[string]string + + // Environment variables for remote servers + EnvVars map[string]string } // AddRunFlags adds all the run flags to a command @@ -131,6 +157,7 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) { []string{}, "Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET)", ) + cmd.Flags().StringVar(&config.RemoteURL, "remote", "", "URL of remote MCP server to run as a workload") cmd.Flags().StringVar(&config.AuthzConfig, "authz-config", "", "Path to the authorization configuration file") cmd.Flags().StringVar(&config.AuditConfig, "audit-config", "", "Path to the audit configuration file") cmd.Flags().BoolVar(&config.EnableAudit, "enable-audit", false, "Enable audit logging with default configuration") @@ -147,6 +174,24 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) { cmd.Flags().BoolVar(&config.JWKSAllowPrivateIP, "jwks-allow-private-ip", false, "Allow JWKS/OIDC endpoints on private IP addresses (use with caution)") + // Remote authentication flags + cmd.Flags().BoolVar(&config.EnableRemoteAuth, "remote-auth", false, + "Enable automatic OAuth/OIDC authentication for remote MCP servers") + cmd.Flags().StringVar(&config.RemoteAuthClientID, "remote-auth-client-id", "", + "OAuth client ID for remote server authentication") + cmd.Flags().StringVar(&config.RemoteAuthClientSecret, "remote-auth-client-secret", "", + "OAuth client secret for remote server authentication") + cmd.Flags().StringVar(&config.RemoteAuthClientSecretFile, "remote-auth-client-secret-file", "", + "Path to file containing client secret for remote server authentication") + cmd.Flags().StringSliceVar(&config.RemoteAuthScopes, "remote-auth-scopes", []string{}, + "OAuth scopes for remote server authentication") + cmd.Flags().BoolVar(&config.RemoteAuthSkipBrowser, "remote-auth-skip-browser", false, + "Skip opening browser for OAuth authentication (use device flow instead)") + cmd.Flags().DurationVar(&config.RemoteAuthTimeout, "remote-auth-timeout", discovery.DefaultOAuthTimeout, + "Timeout for remote authentication flow") + cmd.Flags().IntVar(&config.RemoteAuthCallbackPort, "remote-auth-callback-port", 0, + "Port for OAuth callback (0 = auto-assign)") + // OAuth discovery configuration cmd.Flags().StringVar(&config.ResourceURL, "resource-url", "", "Explicit resource URL for OAuth discovery endpoint (RFC 9728)") @@ -284,22 +329,30 @@ func setupRuntimeAndValidation(ctx context.Context) (runtime.Deployer, runner.En return rt, envVarValidator, nil } -// handleImageRetrieval retrieves and processes the MCP server image +// handleImageRetrieval handles image retrieval and metadata fetching func handleImageRetrieval( - ctx context.Context, serverOrImage string, runFlags *RunFlags, -) (string, *registry.ImageMetadata, error) { - var imageMetadata *registry.ImageMetadata - imageURL := serverOrImage - + ctx context.Context, + serverOrImage string, + runFlags *RunFlags, +) ( + string, + *registry.ImageMetadata, + error, +) { + // Only pull image if we are not running in Kubernetes mode. + // This split will go away if we implement a separate command or binary + // for running MCP servers in Kubernetes. if !runtime.IsKubernetesRuntime() { - var err error - imageURL, imageMetadata, err = retriever.GetMCPServer(ctx, serverOrImage, runFlags.CACertPath, runFlags.VerifyImage) + // Take the MCP server we were supplied and either fetch the image, or + // build it from a protocol scheme. If the server URI refers to an image + // in our trusted registry, we will also fetch the image metadata. + imageURL, imageMetadata, err := retriever.GetMCPServer(ctx, serverOrImage, runFlags.CACertPath, runFlags.VerifyImage) if err != nil { return "", nil, fmt.Errorf("failed to find or create the MCP server %s: %v", serverOrImage, err) } + return imageURL, imageMetadata, nil } - - return imageURL, imageMetadata, nil + return serverOrImage, nil, nil } // validateAndSetupProxyMode validates and sets default proxy mode if needed @@ -329,12 +382,19 @@ func buildRunnerConfig( oidcConfig *auth.TokenValidatorConfig, telemetryConfig *telemetry.Config, ) (*runner.RunConfig, error) { + // Determine transport type + transportType := defaultTransportType + if runFlags.Transport != "" { + transportType = runFlags.Transport + } + // Create a builder for the RunConfig builder := runner.NewRunConfigBuilder(). WithRuntime(rt). WithCmdArgs(cmdArgs). WithName(runFlags.Name). WithImage(imageURL). + WithRemoteURL(runFlags.RemoteURL). WithHost(validatedHost). WithTargetHost(runFlags.TargetHost). WithDebug(debugMode). @@ -346,7 +406,7 @@ func buildRunnerConfig( WithNetworkIsolation(runFlags.IsolateNetwork). WithK8sPodPatch(runFlags.K8sPodPatch). WithProxyMode(types.ProxyMode(runFlags.ProxyMode)). - WithTransportAndPorts(runFlags.Transport, runFlags.ProxyPort, runFlags.TargetPort). + WithTransportAndPorts(transportType, runFlags.ProxyPort, runFlags.TargetPort). WithAuditEnabled(runFlags.EnableAudit, runFlags.AuditConfig). WithLabels(runFlags.Labels). WithGroup(runFlags.Group). @@ -367,6 +427,11 @@ func buildRunnerConfig( runFlags.Transport, ) + // Add remote auth configuration if enabled + if remoteAuthConfig := getRemoteAuthFromFlags(runFlags); remoteAuthConfig != nil { + builder = builder.WithRemoteAuth(remoteAuthConfig) + } + // Load authz config if path is provided if runFlags.AuthzConfig != "" { if authzConfigData, err := authz.LoadConfig(runFlags.AuthzConfig); err == nil { @@ -405,6 +470,25 @@ func extractTelemetryValues(config *telemetry.Config) (string, float64, []string return config.Endpoint, config.SamplingRate, config.EnvironmentVariables } +// getRemoteAuthFromFlags creates RemoteAuthConfig from RunFlags +func getRemoteAuthFromFlags(runFlags *RunFlags) *runner.RemoteAuthConfig { + if runFlags.EnableRemoteAuth || runFlags.RemoteAuthClientID != "" { + return &runner.RemoteAuthConfig{ + ClientID: runFlags.RemoteAuthClientID, + ClientSecret: runFlags.RemoteAuthClientSecret, + Scopes: runFlags.RemoteAuthScopes, + SkipBrowser: runFlags.RemoteAuthSkipBrowser, + Timeout: runFlags.RemoteAuthTimeout, + CallbackPort: runFlags.RemoteAuthCallbackPort, + Issuer: runFlags.RemoteAuthIssuer, + AuthorizeURL: runFlags.RemoteAuthAuthorizeURL, + TokenURL: runFlags.RemoteAuthTokenURL, + OAuthParams: runFlags.OAuthParams, + } + } + return nil +} + // getOidcFromFlags extracts OIDC configuration from command flags func getOidcFromFlags(cmd *cobra.Command) (string, string, string, string, string, string) { oidcIssuer := GetStringFlagOrEmpty(cmd, "oidc-issuer") diff --git a/docs/cli/thv_proxy.md b/docs/cli/thv_proxy.md index cd38f232f..11fae5e33 100644 --- a/docs/cli/thv_proxy.md +++ b/docs/cli/thv_proxy.md @@ -91,7 +91,7 @@ thv proxy [flags] SERVER_NAME --oidc-issuer string OIDC issuer URL (e.g., https://accounts.google.com) --oidc-jwks-url string URL to fetch the JWKS from --port int Port for the HTTP proxy to listen on (host port) - --remote-auth Enable OAuth authentication to remote MCP server + --remote-auth Enable OAuth/OIDC authentication to remote MCP server --remote-auth-authorize-url string OAuth authorization endpoint URL (alternative to --remote-auth-issuer for non-OIDC OAuth) --remote-auth-callback-port int Port for OAuth callback server during remote authentication (default: 8666) (default 8666) --remote-auth-client-id string OAuth client ID for remote server authentication diff --git a/docs/cli/thv_run.md b/docs/cli/thv_run.md index 1c5a033a5..6fd3e7ef3 100644 --- a/docs/cli/thv_run.md +++ b/docs/cli/thv_run.md @@ -17,7 +17,7 @@ Run an MCP server Run an MCP server with the specified name, image, or protocol scheme. -ToolHive supports four ways to run an MCP server: +ToolHive supports five ways to run an MCP server: 1. From the registry: @@ -50,6 +50,14 @@ ToolHive supports four ways to run an MCP server: Runs an MCP server using a previously exported configuration file. +5. Remote MCP server: + + $ thv run --remote [--name ] + + Runs a remote MCP server as a workload, proxying requests to the specified URL. + This allows remote MCP servers to be managed like local workloads with full + support for client configuration, tool filtering, import/export, etc. + The container will be started with the specified transport mode and permission profile. Additional configuration can be provided via flags. @@ -60,48 +68,57 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] ### Options ``` - --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 - --enable-audit Enable audit logging with default configuration - -e, --env stringArray Environment variables to pass to the MCP server (format: KEY=VALUE) - -f, --foreground Run in foreground mode (block until container exits) - --from-config string Load configuration from exported file - --group string Name of the group this workload belongs to (defaults to 'default' if not specified) (default "default") - -h, --help help for run - --host string Host for the HTTP proxy to listen on (IP or hostname) (default "127.0.0.1") - --ignore-globally Load global ignore patterns from ~/.config/toolhive/thvignore (default true) - --image-verification string Set image verification mode (warn, enabled, disabled) (default "warn") - --isolate-network Isolate the container network from the host (default: false) - --jwks-allow-private-ip Allow JWKS/OIDC endpoints on private IP addresses (use with caution) - --jwks-auth-token-file string Path to file containing bearer token for authenticating JWKS/OIDC requests - -l, --label stringArray Set labels on the container (format: key=value) - --name string Name of the MCP server (auto-generated from image if not provided) - --oidc-audience string Expected audience for the token - --oidc-client-id string OIDC client ID - --oidc-client-secret string OIDC client secret (optional, for introspection) - --oidc-introspection-url string URL for token introspection endpoint - --oidc-issuer string OIDC issuer URL (e.g., https://accounts.google.com) - --oidc-jwks-url string URL to fetch the JWKS from - --otel-enable-prometheus-metrics-path Enable Prometheus-style /metrics endpoint on the main transport port - --otel-endpoint string OpenTelemetry OTLP endpoint URL (e.g., https://api.honeycomb.io) - --otel-env-vars stringArray Environment variable names to include in OpenTelemetry spans (comma-separated: ENV1,ENV2) - --otel-headers stringArray OpenTelemetry OTLP headers in key=value format (e.g., x-honeycomb-team=your-api-key) - --otel-insecure Connect to the OpenTelemetry endpoint using HTTP instead of HTTPS - --otel-sampling-rate float OpenTelemetry trace sampling rate (0.0-1.0) (default 0.1) - --otel-service-name string OpenTelemetry service name (defaults to toolhive-mcp-proxy) - --permission-profile string Permission profile to use (none, network, or path to JSON file) - --print-resolved-overlays Debug: show resolved container paths for tmpfs overlays - --proxy-mode string Proxy mode for stdio transport (sse or streamable-http) (default "sse") - --proxy-port int Port for the HTTP proxy to listen on (host port) - --resource-url string Explicit resource URL for OAuth discovery endpoint (RFC 9728) - --secret stringArray Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET) - --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) - --thv-ca-bundle string Path to CA certificate bundle for ToolHive HTTP operations (JWKS, OIDC discovery, etc.) - --tools stringArray Filter MCP server tools (comma-separated list of tool names) - --transport string Transport mode (sse, streamable-http or stdio) - -v, --volume stringArray Mount a volume into the container (format: host-path:container-path[:ro]) + --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 + --enable-audit Enable audit logging with default configuration + -e, --env stringArray Environment variables to pass to the MCP server (format: KEY=VALUE) + -f, --foreground Run in foreground mode (block until container exits) + --from-config string Load configuration from exported file + --group string Name of the group this workload belongs to (defaults to 'default' if not specified) (default "default") + -h, --help help for run + --host string Host for the HTTP proxy to listen on (IP or hostname) (default "127.0.0.1") + --ignore-globally Load global ignore patterns from ~/.config/toolhive/thvignore (default true) + --image-verification string Set image verification mode (warn, enabled, disabled) (default "warn") + --isolate-network Isolate the container network from the host (default: false) + --jwks-allow-private-ip Allow JWKS/OIDC endpoints on private IP addresses (use with caution) + --jwks-auth-token-file string Path to file containing bearer token for authenticating JWKS/OIDC requests + -l, --label stringArray Set labels on the container (format: key=value) + --name string Name of the MCP server (auto-generated from image if not provided) + --oidc-audience string Expected audience for the token + --oidc-client-id string OIDC client ID + --oidc-client-secret string OIDC client secret (optional, for introspection) + --oidc-introspection-url string URL for token introspection endpoint + --oidc-issuer string OIDC issuer URL (e.g., https://accounts.google.com) + --oidc-jwks-url string URL to fetch the JWKS from + --otel-enable-prometheus-metrics-path Enable Prometheus-style /metrics endpoint on the main transport port + --otel-endpoint string OpenTelemetry OTLP endpoint URL (e.g., https://api.honeycomb.io) + --otel-env-vars stringArray Environment variable names to include in OpenTelemetry spans (comma-separated: ENV1,ENV2) + --otel-headers stringArray OpenTelemetry OTLP headers in key=value format (e.g., x-honeycomb-team=your-api-key) + --otel-insecure Connect to the OpenTelemetry endpoint using HTTP instead of HTTPS + --otel-sampling-rate float OpenTelemetry trace sampling rate (0.0-1.0) (default 0.1) + --otel-service-name string OpenTelemetry service name (defaults to toolhive-mcp-proxy) + --permission-profile string Permission profile to use (none, network, or path to JSON file) + --print-resolved-overlays Debug: show resolved container paths for tmpfs overlays + --proxy-mode string Proxy mode for stdio transport (sse or streamable-http) (default "sse") + --proxy-port int Port for the HTTP proxy to listen on (host port) + --remote string URL of remote MCP server to run as a workload + --remote-auth Enable automatic OAuth/OIDC authentication for remote MCP servers + --remote-auth-callback-port int Port for OAuth callback (0 = auto-assign) + --remote-auth-client-id string OAuth client ID for remote server authentication + --remote-auth-client-secret string OAuth client secret for remote server authentication + --remote-auth-client-secret-file string Path to file containing client secret for remote server authentication + --remote-auth-scopes strings OAuth scopes for remote server authentication + --remote-auth-skip-browser Skip opening browser for OAuth authentication (use device flow instead) + --remote-auth-timeout duration Timeout for remote authentication flow (default 5m0s) + --resource-url string Explicit resource URL for OAuth discovery endpoint (RFC 9728) + --secret stringArray Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET) + --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) + --thv-ca-bundle string Path to CA certificate bundle for ToolHive HTTP operations (JWKS, OIDC discovery, etc.) + --tools stringArray Filter MCP server tools (comma-separated list of tool names) + --transport string Transport mode (sse, streamable-http or stdio) + -v, --volume stringArray Mount a volume into the container (format: host-path:container-path[:ro]) ``` ### Options inherited from parent commands diff --git a/docs/server/docs.go b/docs/server/docs.go index 5f37ec6ec..597d65c46 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -6,7 +6,7 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"}}}, + "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"enableRemoteAuth":{"type":"boolean"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"}}}, "info": {"description":"{{escape .Description}}","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"","url":""}, "paths": {"/api/openapi.json":{"get":{"description":"Returns the OpenAPI specification for the API","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI specification"}},"summary":"Get OpenAPI specification","tags":["system"]}},"/api/v1beta/clients":{"get":{"description":"List all registered clients in ToolHive","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/client.RegisteredClient"},"type":"array"}}},"description":"OK"}},"summary":"List all clients","tags":["clients"]},"post":{"description":"Register a new client with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientRequest"}}},"description":"Client to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register a new client","tags":["clients"]}},"/api/v1beta/clients/register":{"post":{"description":"Register multiple clients with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/v1.createClientResponse"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register multiple clients","tags":["clients"]}},"/api/v1beta/clients/unregister":{"post":{"description":"Unregister multiple clients from ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to unregister","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister multiple clients","tags":["clients"]}},"/api/v1beta/clients/{name}":{"delete":{"description":"Unregister a client from ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister a client","tags":["clients"]}},"/api/v1beta/clients/{name}/groups/{group}":{"delete":{"description":"Unregister a client from a specific group in ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Group name to remove client from","in":"path","name":"group","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Client or group not found"}},"summary":"Unregister a client from a specific group","tags":["clients"]}},"/api/v1beta/discovery/clients":{"get":{"description":"List all clients compatible with ToolHive and their status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.clientStatusResponse"}}},"description":"OK"}},"summary":"List all clients status","tags":["discovery"]}},"/api/v1beta/groups":{"get":{"description":"Get a list of all groups","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.groupListResponse"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List all groups","tags":["groups"]},"post":{"description":"Create a new group with the specified name","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupRequest"}}},"description":"Group creation request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new group","tags":["groups"]}},"/api/v1beta/groups/{name}":{"delete":{"description":"Delete a group by name.","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Delete all workloads in the group (default: false, moves workloads to default group)","in":"query","name":"with-workloads","schema":{"type":"boolean"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a group","tags":["groups"]},"get":{"description":"Get details of a specific group","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/groups.Group"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get group details","tags":["groups"]}},"/api/v1beta/registry":{"get":{"description":"Get a list of the current registries","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.registryListResponse"}}},"description":"OK"}},"summary":"List registries","tags":["registry"]},"post":{"description":"Add a new registry","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"501":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Implemented"}},"summary":"Add a registry","tags":["registry"]}},"/api/v1beta/registry/{name}":{"delete":{"description":"Remove a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Remove a registry","tags":["registry"]},"get":{"description":"Get details of a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getRegistryResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a registry","tags":["registry"]},"put":{"description":"Update registry URL or local path for the default registry","parameters":[{"description":"Registry name (must be 'default')","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryRequest"}}},"description":"Registry configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update registry configuration","tags":["registry"]}},"/api/v1beta/registry/{name}/servers":{"get":{"description":"Get a list of servers in a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listServersResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"List servers in a registry","tags":["registry"]}},"/api/v1beta/registry/{name}/servers/{serverName}":{"get":{"description":"Get details of a specific server in a registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"ImageMetadata name","in":"path","name":"serverName","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getServerResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a server from a registry","tags":["registry"]}},"/api/v1beta/secrets":{"post":{"description":"Setup the secrets provider with the specified type and configuration.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsRequest"}}},"description":"Setup secrets provider request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Setup or reconfigure secrets provider","tags":["secrets"]}},"/api/v1beta/secrets/default":{"get":{"description":"Get details of the default secrets provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getSecretsProviderResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get secrets provider details","tags":["secrets"]}},"/api/v1beta/secrets/default/keys":{"get":{"description":"Get a list of all secret keys from the default provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listSecretsResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support listing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List secrets","tags":["secrets"]},"post":{"description":"Create a new secret in the default provider (encrypted provider only)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretRequest"}}},"description":"Create secret request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict - Secret already exists"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new secret","tags":["secrets"]}},"/api/v1beta/secrets/default/keys/{key}":{"delete":{"description":"Delete a secret from the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support deletion"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a secret","tags":["secrets"]},"put":{"description":"Update an existing secret in the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretRequest"}}},"description":"Update secret request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Update a secret","tags":["secrets"]}},"/api/v1beta/version":{"get":{"description":"Returns the current version of the server","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.versionResponse"}}},"description":"OK"}},"summary":"Get server version","tags":["version"]}},"/api/v1beta/workloads":{"get":{"description":"Get a list of all running workloads, optionally filtered by group","parameters":[{"description":"List all workloads, including stopped ones","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter workloads by group name","in":"query","name":"group","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadListResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Group not found"}},"summary":"List all workloads","tags":["workloads"]},"post":{"description":"Create and start a new workload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"Create workload request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"}},"summary":"Create a new workload","tags":["workloads"]}},"/api/v1beta/workloads/delete":{"post":{"description":"Delete multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk delete request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Delete workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/restart":{"post":{"description":"Restart multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk restart request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Restart workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/stop":{"post":{"description":"Stop multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk stop request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Stop workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/{name}":{"delete":{"description":"Delete a workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Delete a workload","tags":["workloads"]},"get":{"description":"Get details of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload details","tags":["workloads"]}},"/api/v1beta/workloads/{name}/edit":{"post":{"description":"Update an existing workload configuration","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateRequest"}}},"description":"Update workload request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/export":{"get":{"description":"Export a workload's run configuration as JSON","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/runner.RunConfig"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Export workload configuration","tags":["workloads"]}},"/api/v1beta/workloads/{name}/logs":{"get":{"description":"Retrieve at most 100 lines of logs for a specific workload by name.","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}},"description":"Logs for the specified workload"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/restart":{"post":{"description":"Restart a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Restart a workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/stop":{"post":{"description":"Stop a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Stop a workload","tags":["workloads"]}},"/health":{"get":{"description":"Check if the API is healthy","responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"}},"summary":"Health check","tags":["system"]}}}, diff --git a/docs/server/swagger.json b/docs/server/swagger.json index 8e0b57160..2b9597516 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -1,5 +1,5 @@ { - "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"}}}, + "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"enableRemoteAuth":{"type":"boolean"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"}}}, "info": {"description":"This is the ToolHive API server.","title":"ToolHive API","version":"1.0"}, "externalDocs": {"description":"","url":""}, "paths": {"/api/openapi.json":{"get":{"description":"Returns the OpenAPI specification for the API","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI specification"}},"summary":"Get OpenAPI specification","tags":["system"]}},"/api/v1beta/clients":{"get":{"description":"List all registered clients in ToolHive","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/client.RegisteredClient"},"type":"array"}}},"description":"OK"}},"summary":"List all clients","tags":["clients"]},"post":{"description":"Register a new client with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientRequest"}}},"description":"Client to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register a new client","tags":["clients"]}},"/api/v1beta/clients/register":{"post":{"description":"Register multiple clients with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/v1.createClientResponse"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register multiple clients","tags":["clients"]}},"/api/v1beta/clients/unregister":{"post":{"description":"Unregister multiple clients from ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to unregister","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister multiple clients","tags":["clients"]}},"/api/v1beta/clients/{name}":{"delete":{"description":"Unregister a client from ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister a client","tags":["clients"]}},"/api/v1beta/clients/{name}/groups/{group}":{"delete":{"description":"Unregister a client from a specific group in ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Group name to remove client from","in":"path","name":"group","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Client or group not found"}},"summary":"Unregister a client from a specific group","tags":["clients"]}},"/api/v1beta/discovery/clients":{"get":{"description":"List all clients compatible with ToolHive and their status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.clientStatusResponse"}}},"description":"OK"}},"summary":"List all clients status","tags":["discovery"]}},"/api/v1beta/groups":{"get":{"description":"Get a list of all groups","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.groupListResponse"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List all groups","tags":["groups"]},"post":{"description":"Create a new group with the specified name","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupRequest"}}},"description":"Group creation request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new group","tags":["groups"]}},"/api/v1beta/groups/{name}":{"delete":{"description":"Delete a group by name.","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Delete all workloads in the group (default: false, moves workloads to default group)","in":"query","name":"with-workloads","schema":{"type":"boolean"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a group","tags":["groups"]},"get":{"description":"Get details of a specific group","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/groups.Group"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get group details","tags":["groups"]}},"/api/v1beta/registry":{"get":{"description":"Get a list of the current registries","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.registryListResponse"}}},"description":"OK"}},"summary":"List registries","tags":["registry"]},"post":{"description":"Add a new registry","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"501":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Implemented"}},"summary":"Add a registry","tags":["registry"]}},"/api/v1beta/registry/{name}":{"delete":{"description":"Remove a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Remove a registry","tags":["registry"]},"get":{"description":"Get details of a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getRegistryResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a registry","tags":["registry"]},"put":{"description":"Update registry URL or local path for the default registry","parameters":[{"description":"Registry name (must be 'default')","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryRequest"}}},"description":"Registry configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update registry configuration","tags":["registry"]}},"/api/v1beta/registry/{name}/servers":{"get":{"description":"Get a list of servers in a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listServersResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"List servers in a registry","tags":["registry"]}},"/api/v1beta/registry/{name}/servers/{serverName}":{"get":{"description":"Get details of a specific server in a registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"ImageMetadata name","in":"path","name":"serverName","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getServerResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a server from a registry","tags":["registry"]}},"/api/v1beta/secrets":{"post":{"description":"Setup the secrets provider with the specified type and configuration.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsRequest"}}},"description":"Setup secrets provider request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Setup or reconfigure secrets provider","tags":["secrets"]}},"/api/v1beta/secrets/default":{"get":{"description":"Get details of the default secrets provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getSecretsProviderResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get secrets provider details","tags":["secrets"]}},"/api/v1beta/secrets/default/keys":{"get":{"description":"Get a list of all secret keys from the default provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listSecretsResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support listing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List secrets","tags":["secrets"]},"post":{"description":"Create a new secret in the default provider (encrypted provider only)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretRequest"}}},"description":"Create secret request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict - Secret already exists"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new secret","tags":["secrets"]}},"/api/v1beta/secrets/default/keys/{key}":{"delete":{"description":"Delete a secret from the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support deletion"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a secret","tags":["secrets"]},"put":{"description":"Update an existing secret in the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretRequest"}}},"description":"Update secret request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Update a secret","tags":["secrets"]}},"/api/v1beta/version":{"get":{"description":"Returns the current version of the server","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.versionResponse"}}},"description":"OK"}},"summary":"Get server version","tags":["version"]}},"/api/v1beta/workloads":{"get":{"description":"Get a list of all running workloads, optionally filtered by group","parameters":[{"description":"List all workloads, including stopped ones","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter workloads by group name","in":"query","name":"group","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadListResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Group not found"}},"summary":"List all workloads","tags":["workloads"]},"post":{"description":"Create and start a new workload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"Create workload request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"}},"summary":"Create a new workload","tags":["workloads"]}},"/api/v1beta/workloads/delete":{"post":{"description":"Delete multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk delete request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Delete workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/restart":{"post":{"description":"Restart multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk restart request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Restart workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/stop":{"post":{"description":"Stop multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk stop request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Stop workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/{name}":{"delete":{"description":"Delete a workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Delete a workload","tags":["workloads"]},"get":{"description":"Get details of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload details","tags":["workloads"]}},"/api/v1beta/workloads/{name}/edit":{"post":{"description":"Update an existing workload configuration","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateRequest"}}},"description":"Update workload request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/export":{"get":{"description":"Export a workload's run configuration as JSON","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/runner.RunConfig"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Export workload configuration","tags":["workloads"]}},"/api/v1beta/workloads/{name}/logs":{"get":{"description":"Retrieve at most 100 lines of logs for a specific workload by name.","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}},"description":"Logs for the specified workload"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/restart":{"post":{"description":"Restart a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Restart a workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/stop":{"post":{"description":"Stop a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Stop a workload","tags":["workloads"]}},"/health":{"get":{"description":"Check if the API is healthy","responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"}},"summary":"Health check","tags":["system"]}}}, diff --git a/docs/server/swagger.yaml b/docs/server/swagger.yaml index 6ec8e3ed0..310854c41 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -459,6 +459,11 @@ components: AuthorizeURL is the OAuth authorization endpoint URL Used for non-OIDC OAuth flows when issuer is not provided type: string + callback_port: + description: |- + CallbackPort is the specific port to use for the OAuth callback server + If not specified, a random available port will be used + type: integer client_id: description: ClientID is the OAuth client ID for authentication type: string @@ -467,6 +472,13 @@ components: Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com) Used for OIDC discovery to find authorization and token endpoints type: string + oauth_params: + additionalProperties: + type: string + description: |- + OAuthParams contains additional OAuth parameters to include in the authorization request + These are server-specific parameters like "prompt", "response_mode", etc. + type: object scopes: description: |- Scopes are the OAuth scopes to request @@ -605,6 +617,41 @@ components: predicate_type: type: string type: object + runner.RemoteAuthConfig: + description: RemoteAuthConfig contains OAuth configuration for remote MCP servers + properties: + authorizeURL: + type: string + callbackPort: + type: integer + clientID: + type: string + clientSecret: + type: string + clientSecretFile: + type: string + enableRemoteAuth: + type: boolean + issuer: + description: OAuth endpoint configuration (from registry) + type: string + oauthParams: + additionalProperties: + type: string + description: OAuth parameters for server-specific customization + type: object + scopes: + items: + type: string + type: array + skipBrowser: + type: boolean + timeout: + example: 5m + type: string + tokenURL: + type: string + type: object runner.RunConfig: properties: audit_config: @@ -692,6 +739,11 @@ components: type: integer proxy_mode: $ref: '#/components/schemas/types.ProxyMode' + remote_auth_config: + $ref: '#/components/schemas/runner.RemoteAuthConfig' + remote_url: + description: RemoteURL is the URL of the remote MCP server (if running remotely) + type: string schema_version: description: SchemaVersion is the version of the RunConfig schema type: string diff --git a/pkg/auth/discovery/discovery.go b/pkg/auth/discovery/discovery.go new file mode 100644 index 000000000..6caf701ca --- /dev/null +++ b/pkg/auth/discovery/discovery.go @@ -0,0 +1,345 @@ +// Package discovery provides authentication discovery utilities for detecting +// authentication requirements from remote servers. +// +// Supported Authentication Types: +// - OAuth 2.0 with PKCE (Proof Key for Code Exchange) +// - OIDC (OpenID Connect) discovery +// - Manual OAuth endpoint configuration +package discovery + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" + + "github.com/stacklok/toolhive/pkg/auth/oauth" + "github.com/stacklok/toolhive/pkg/logger" +) + +// Default timeout constants for authentication operations +const ( + DefaultOAuthTimeout = 5 * time.Minute + DefaultHTTPTimeout = 30 * time.Second + DefaultAuthDetectTimeout = 10 * time.Second + MaxRetryAttempts = 3 + RetryBaseDelay = 2 * time.Second +) + +// AuthInfo contains authentication information extracted from WWW-Authenticate header +type AuthInfo struct { + Realm string + Type string + ResourceMetadata string + Error string + ErrorDescription string +} + +// Config holds configuration for authentication discovery +type Config struct { + Timeout time.Duration + TLSHandshakeTimeout time.Duration + ResponseHeaderTimeout time.Duration + EnablePOSTDetection bool // Whether to try POST requests for detection +} + +// DefaultDiscoveryConfig returns a default discovery configuration +func DefaultDiscoveryConfig() *Config { + return &Config{ + Timeout: DefaultAuthDetectTimeout, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + EnablePOSTDetection: true, + } +} + +// DetectAuthenticationFromServer attempts to detect authentication requirements from the target server +func DetectAuthenticationFromServer(ctx context.Context, targetURI string, config *Config) (*AuthInfo, error) { + if config == nil { + config = DefaultDiscoveryConfig() + } + + // Create a context with timeout for auth detection + detectCtx, cancel := context.WithTimeout(ctx, config.Timeout) + defer cancel() + + // Make a test request to the target server to see if it returns WWW-Authenticate + client := &http.Client{ + Timeout: config.Timeout, + Transport: &http.Transport{ + TLSHandshakeTimeout: config.TLSHandshakeTimeout, + ResponseHeaderTimeout: config.ResponseHeaderTimeout, + }, + } + + // First try a GET request + authInfo, err := detectAuthWithRequest(detectCtx, client, targetURI, http.MethodGet, nil) + if err != nil { + return nil, err + } + if authInfo != nil { + return authInfo, nil + } + + // If no auth detected with GET and POST detection is enabled, try a POST request with JSON-RPC initialize + // Some servers only return WWW-Authenticate on specific requests + if config.EnablePOSTDetection { + postBody := strings.NewReader(`{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}`) + authInfo, err = detectAuthWithRequest(detectCtx, client, targetURI, http.MethodPost, postBody) + if err != nil { + return nil, err + } + if authInfo != nil { + return authInfo, nil + } + } + + return nil, nil // No authentication required +} + +// detectAuthWithRequest makes a specific HTTP request and checks for authentication requirements +func detectAuthWithRequest( + ctx context.Context, + client *http.Client, + targetURI string, + method string, + body *strings.Reader, +) (*AuthInfo, error) { + var req *http.Request + var err error + + if body != nil { + req, err = http.NewRequestWithContext(ctx, method, targetURI, body) + if err != nil { + return nil, fmt.Errorf("failed to create %s request: %w", method, err) + } + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequestWithContext(ctx, method, targetURI, nil) + if err != nil { + return nil, fmt.Errorf("failed to create %s request: %w", method, err) + } + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make %s request: %w", method, err) + } + defer resp.Body.Close() + + // Check if we got a 401 Unauthorized with WWW-Authenticate header + if resp.StatusCode == http.StatusUnauthorized { + wwwAuth := resp.Header.Get("WWW-Authenticate") + if wwwAuth != "" { + return ParseWWWAuthenticate(wwwAuth) + } + } + + return nil, nil +} + +// ParseWWWAuthenticate parses the WWW-Authenticate header to extract authentication information +// Supports multiple authentication schemes and complex header formats +func ParseWWWAuthenticate(header string) (*AuthInfo, error) { + // Trim whitespace and handle empty headers + header = strings.TrimSpace(header) + if header == "" { + return nil, fmt.Errorf("empty WWW-Authenticate header") + } + + // Split by comma to handle multiple authentication schemes + schemes := strings.Split(header, ",") + + for _, scheme := range schemes { + scheme = strings.TrimSpace(scheme) + + // Check for OAuth/Bearer authentication + if strings.HasPrefix(scheme, "Bearer") { + authInfo := &AuthInfo{Type: "OAuth"} + + // Extract parameters after "Bearer" + params := strings.TrimSpace(strings.TrimPrefix(scheme, "Bearer")) + if params != "" { + // Parse parameters (realm, scope, etc.) + realm := ExtractParameter(params, "realm") + if realm != "" { + authInfo.Realm = realm + } + } + + return authInfo, nil + } + + // Check for OAuth-specific schemes + if strings.HasPrefix(scheme, "OAuth") { + authInfo := &AuthInfo{Type: "OAuth"} + + // Extract parameters after "OAuth" + params := strings.TrimSpace(strings.TrimPrefix(scheme, "OAuth")) + if params != "" { + // Parse parameters (realm, scope, etc.) + realm := ExtractParameter(params, "realm") + if realm != "" { + authInfo.Realm = realm + } + } + + return authInfo, nil + } + + // Currently only OAuth-based authentication is supported + // Basic and Digest authentication are not implemented + logger.Debugf("Unsupported authentication scheme: %s", scheme) + } + + return nil, fmt.Errorf("no supported authentication type found in header: %s", header) +} + +// ExtractParameter extracts a parameter value from an authentication header +func ExtractParameter(params, paramName string) string { + // Look for paramName=value or paramName="value" + // Split by comma first, then by space to handle multiple parameters + parts := strings.Split(params, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, paramName+"=") { + value := strings.TrimPrefix(part, paramName+"=") + // Remove quotes if present + value = strings.Trim(value, `"`) + return value + } + } + return "" +} + +// DeriveIssuerFromURL attempts to derive the OAuth issuer from the remote URL using general patterns +func DeriveIssuerFromURL(remoteURL string) string { + // Parse the URL to extract the domain + parsedURL, err := url.Parse(remoteURL) + if err != nil { + logger.Debugf("Failed to parse remote URL: %v", err) + return "" + } + + host := parsedURL.Hostname() + if host == "" { + return "" + } + + // General pattern: use the domain as the issuer + // This works for most OAuth providers that use their domain as the issuer + issuer := fmt.Sprintf("https://%s", host) + + logger.Debugf("Derived issuer from URL - remoteURL: %s, issuer: %s", remoteURL, issuer) + return issuer +} + +// OAuthFlowConfig contains configuration for performing OAuth flows +type OAuthFlowConfig struct { + ClientID string + ClientSecret string + AuthorizeURL string // Manual OAuth endpoint (optional) + TokenURL string // Manual OAuth endpoint (optional) + Scopes []string + CallbackPort int + Timeout time.Duration + SkipBrowser bool + OAuthParams map[string]string +} + +// OAuthFlowResult contains the result of an OAuth flow +type OAuthFlowResult struct { + TokenSource *oauth2.TokenSource + Config *oauth.Config +} + +// PerformOAuthFlow performs an OAuth authentication flow with the given configuration +func PerformOAuthFlow(ctx context.Context, issuer string, config *OAuthFlowConfig) (*OAuthFlowResult, error) { + logger.Infof("Starting OAuth authentication flow for issuer: %s", issuer) + + if config == nil { + return nil, fmt.Errorf("OAuth flow config cannot be nil") + } + + var oauthConfig *oauth.Config + var err error + + // Check if we have manual OAuth endpoints configured + if config.AuthorizeURL != "" && config.TokenURL != "" { + logger.Infof("Using manual OAuth endpoints - authorize_url: %s, token_url: %s", + config.AuthorizeURL, config.TokenURL) + + oauthConfig, err = oauth.CreateOAuthConfigManual( + config.ClientID, + config.ClientSecret, + config.AuthorizeURL, + config.TokenURL, + config.Scopes, + true, // Enable PKCE by default for security + config.CallbackPort, + config.OAuthParams, + ) + } else { + // Fall back to OIDC discovery + logger.Info("Using OIDC discovery") + oauthConfig, err = oauth.CreateOAuthConfigFromOIDC( + ctx, + issuer, + config.ClientID, + config.ClientSecret, + config.Scopes, + true, // Enable PKCE by default for security + config.CallbackPort, + ) + } + + if err != nil { + return nil, fmt.Errorf("failed to create OAuth config: %w", err) + } + + // Create OAuth flow + flow, err := oauth.NewFlow(oauthConfig) + if err != nil { + return nil, fmt.Errorf("failed to create OAuth flow: %w", err) + } + + // Create a context with timeout for the OAuth flow + oauthTimeout := config.Timeout + if oauthTimeout <= 0 { + oauthTimeout = DefaultOAuthTimeout + } + + oauthCtx, cancel := context.WithTimeout(ctx, oauthTimeout) + defer cancel() + + // Start OAuth flow + tokenResult, err := flow.Start(oauthCtx, config.SkipBrowser) + if err != nil { + if oauthCtx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("OAuth flow timed out after %v - user did not complete authentication", oauthTimeout) + } + return nil, fmt.Errorf("OAuth flow failed: %w", err) + } + + logger.Info("OAuth authentication successful") + + // Log token info (without exposing the actual token) + if tokenResult.Claims != nil { + if sub, ok := tokenResult.Claims["sub"].(string); ok { + logger.Infof("Authenticated as subject: %s", sub) + } + if email, ok := tokenResult.Claims["email"].(string); ok { + logger.Infof("Authenticated email: %s", email) + } + } + + source := flow.TokenSource() + return &OAuthFlowResult{ + TokenSource: &source, + Config: oauthConfig, + }, nil +} diff --git a/pkg/auth/discovery/discovery_test.go b/pkg/auth/discovery/discovery_test.go new file mode 100644 index 000000000..224a3c8b9 --- /dev/null +++ b/pkg/auth/discovery/discovery_test.go @@ -0,0 +1,357 @@ +package discovery + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stacklok/toolhive/pkg/logger" +) + +func init() { + // Initialize logger for tests + logger.Initialize() +} + +func TestParseWWWAuthenticate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + header string + expected *AuthInfo + wantErr bool + }{ + { + name: "empty header", + header: "", + wantErr: true, + }, + { + name: "whitespace only", + header: " ", + wantErr: true, + }, + { + name: "simple bearer", + header: "Bearer", + expected: &AuthInfo{ + Type: "OAuth", + }, + }, + { + name: "bearer with realm", + header: `Bearer realm="https://example.com"`, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com", + }, + }, + { + name: "bearer with quoted realm", + header: `Bearer realm="https://example.com/oauth"`, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com/oauth", + }, + }, + { + name: "oauth scheme", + header: `OAuth realm="https://example.com"`, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com", + }, + }, + { + name: "multiple schemes with bearer first", + header: `Bearer realm="https://example.com", Basic realm="test"`, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com", + }, + }, + { + name: "unsupported scheme", + header: "Basic realm=\"test\"", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result, err := ParseWWWAuthenticate(tt.header) + + if tt.wantErr { + if err == nil { + t.Errorf("ParseWWWAuthenticate() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("ParseWWWAuthenticate() unexpected error: %v", err) + return + } + + if result.Type != tt.expected.Type { + t.Errorf("ParseWWWAuthenticate() Type = %v, want %v", result.Type, tt.expected.Type) + } + + if result.Realm != tt.expected.Realm { + t.Errorf("ParseWWWAuthenticate() Realm = %v, want %v", result.Realm, tt.expected.Realm) + } + }) + } +} + +func TestExtractParameter(t *testing.T) { + t.Parallel() + tests := []struct { + name string + params string + paramName string + expected string + }{ + { + name: "simple parameter", + params: `realm="https://example.com"`, + paramName: "realm", + expected: "https://example.com", + }, + { + name: "quoted parameter", + params: `realm="https://example.com/oauth"`, + paramName: "realm", + expected: "https://example.com/oauth", + }, + { + name: "multiple parameters", + params: `realm="https://example.com", scope="openid"`, + paramName: "realm", + expected: "https://example.com", + }, + { + name: "parameter not found", + params: `realm="https://example.com"`, + paramName: "scope", + expected: "", + }, + { + name: "empty params", + params: "", + paramName: "realm", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ExtractParameter(tt.params, tt.paramName) + if result != tt.expected { + t.Errorf("ExtractParameter() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestDeriveIssuerFromURL(t *testing.T) { + t.Parallel() + tests := []struct { + name string + url string + expected string + }{ + { + name: "https url", + url: "https://example.com/api", + expected: "https://example.com", + }, + { + name: "http url", + url: "http://localhost:8080/api", + expected: "https://localhost", + }, + { + name: "url with path", + url: "https://api.example.com/v1/endpoint", + expected: "https://api.example.com", + }, + { + name: "url with query params", + url: "https://example.com/api?param=value", + expected: "https://example.com", + }, + { + name: "invalid url", + url: "not-a-url", + expected: "", + }, + { + name: "empty url", + url: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := DeriveIssuerFromURL(tt.url) + if result != tt.expected { + t.Errorf("DeriveIssuerFromURL() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestDetectAuthenticationFromServer(t *testing.T) { + t.Parallel() + tests := []struct { + name string + serverResponse func(w http.ResponseWriter, _ *http.Request) + expected *AuthInfo + wantErr bool + }{ + { + name: "no authentication required", + serverResponse: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }, + expected: nil, + }, + { + name: "bearer authentication required", + serverResponse: func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer realm="https://example.com"`) + w.WriteHeader(http.StatusUnauthorized) + }, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com", + }, + }, + { + name: "oauth authentication required", + serverResponse: func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("WWW-Authenticate", `OAuth realm="https://example.com"`) + w.WriteHeader(http.StatusUnauthorized) + }, + expected: &AuthInfo{ + Type: "OAuth", + Realm: "https://example.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Create test server + server := httptest.NewServer(http.HandlerFunc(tt.serverResponse)) + defer server.Close() + + // Test detection + ctx := context.Background() + result, err := DetectAuthenticationFromServer(ctx, server.URL, nil) + + if tt.wantErr { + if err == nil { + t.Errorf("DetectAuthenticationFromServer() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("DetectAuthenticationFromServer() unexpected error: %v", err) + return + } + + if tt.expected == nil { + if result != nil { + t.Errorf("DetectAuthenticationFromServer() = %v, want nil", result) + } + return + } + + if result == nil { + t.Errorf("DetectAuthenticationFromServer() = nil, want %v", tt.expected) + return + } + + if result.Type != tt.expected.Type { + t.Errorf("DetectAuthenticationFromServer() Type = %v, want %v", result.Type, tt.expected.Type) + } + + if result.Realm != tt.expected.Realm { + t.Errorf("DetectAuthenticationFromServer() Realm = %v, want %v", result.Realm, tt.expected.Realm) + } + }) + } +} + +func TestDefaultDiscoveryConfig(t *testing.T) { + t.Parallel() + config := DefaultDiscoveryConfig() + + if config.Timeout != 10*time.Second { + t.Errorf("DefaultDiscoveryConfig() Timeout = %v, want %v", config.Timeout, 10*time.Second) + } + + if config.TLSHandshakeTimeout != 5*time.Second { + t.Errorf("DefaultDiscoveryConfig() TLSHandshakeTimeout = %v, want %v", config.TLSHandshakeTimeout, 5*time.Second) + } + + if config.ResponseHeaderTimeout != 5*time.Second { + t.Errorf("DefaultDiscoveryConfig() ResponseHeaderTimeout = %v, want %v", config.ResponseHeaderTimeout, 5*time.Second) + } + + if !config.EnablePOSTDetection { + t.Errorf("DefaultDiscoveryConfig() EnablePOSTDetection = %v, want %v", config.EnablePOSTDetection, true) + } +} + +func TestOAuthFlowConfig(t *testing.T) { + t.Parallel() + t.Run("nil config validation", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + result, err := PerformOAuthFlow(ctx, "https://example.com", nil) + + if err == nil { + t.Errorf("PerformOAuthFlow() expected error for nil config but got none") + } + if result != nil { + t.Errorf("PerformOAuthFlow() expected nil result for nil config") + } + if !strings.Contains(err.Error(), "OAuth flow config cannot be nil") { + t.Errorf("PerformOAuthFlow() expected nil config error, got: %v", err) + } + }) + + t.Run("config validation", func(t *testing.T) { + t.Parallel() + config := &OAuthFlowConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"openid"}, + } + + // This test only validates that the config is accepted and doesn't cause + // immediate validation errors. The actual OAuth flow will fail with OIDC + // discovery errors, which is expected. + if config.ClientID == "" { + t.Errorf("Expected ClientID to be set") + } + if config.ClientSecret == "" { + t.Errorf("Expected ClientSecret to be set") + } + if len(config.Scopes) == 0 { + t.Errorf("Expected Scopes to be set") + } + }) +} diff --git a/pkg/auth/oauth/flow.go b/pkg/auth/oauth/flow.go index 371fdf902..435b4aa31 100644 --- a/pkg/auth/oauth/flow.go +++ b/pkg/auth/oauth/flow.go @@ -51,6 +51,9 @@ type Config struct { // IntrospectionEndpoint is the optional introspection endpoint for validating tokens IntrospectionEndpoint string + + // OAuthParams are additional parameters to pass to the authorization URL + OAuthParams map[string]string } // Flow handles the OAuth authentication flow @@ -241,6 +244,13 @@ func (f *Flow) buildAuthURL() string { oauth2.SetAuthURLParam("state", f.state), } + // Add registry-provided OAuth parameters + if f.config.OAuthParams != nil { + for key, value := range f.config.OAuthParams { + opts = append(opts, oauth2.SetAuthURLParam(key, value)) + } + } + // Add PKCE parameters if enabled if f.config.UsePKCE { opts = append(opts, diff --git a/pkg/auth/oauth/manual.go b/pkg/auth/oauth/manual.go index 310e42832..d7ad58d8f 100644 --- a/pkg/auth/oauth/manual.go +++ b/pkg/auth/oauth/manual.go @@ -14,6 +14,7 @@ func CreateOAuthConfigManual( scopes []string, usePKCE bool, callbackPort int, + oauthParams map[string]string, ) (*Config, error) { if clientID == "" { return nil, fmt.Errorf("client ID is required") @@ -46,5 +47,6 @@ func CreateOAuthConfigManual( Scopes: scopes, UsePKCE: usePKCE, CallbackPort: callbackPort, + OAuthParams: oauthParams, }, nil } diff --git a/pkg/auth/oauth/manual_test.go b/pkg/auth/oauth/manual_test.go index d90634639..637ad88a2 100644 --- a/pkg/auth/oauth/manual_test.go +++ b/pkg/auth/oauth/manual_test.go @@ -99,16 +99,38 @@ func TestCreateOAuthConfigManual(t *testing.T) { name: "127.0.0.1 URLs allowed for development", clientID: "test-client", clientSecret: "test-secret", - authURL: "http://127.0.0.1:3000/auth", - tokenURL: "http://127.0.0.1:3000/token", + authURL: "http://127.0.0.1:8080/oauth/authorize", + tokenURL: "http://127.0.0.1:8080/oauth/token", scopes: []string{"read"}, - usePKCE: false, + usePKCE: true, callbackPort: 8080, expectError: false, validate: func(t *testing.T, config *Config) { t.Helper() - assert.Equal(t, "http://127.0.0.1:3000/auth", config.AuthURL) - assert.Equal(t, "http://127.0.0.1:3000/token", config.TokenURL) + assert.Equal(t, "http://127.0.0.1:8080/oauth/authorize", config.AuthURL) + assert.Equal(t, "http://127.0.0.1:8080/oauth/token", config.TokenURL) + }, + }, + { + name: "valid config with OAuth parameters", + clientID: "test-client", + clientSecret: "test-secret", + authURL: "https://example.com/oauth/authorize", + tokenURL: "https://example.com/oauth/token", + scopes: []string{"read", "write"}, + usePKCE: true, + callbackPort: 8080, + expectError: false, + validate: func(t *testing.T, config *Config) { + t.Helper() + assert.Equal(t, "test-client", config.ClientID) + assert.Equal(t, "test-secret", config.ClientSecret) + assert.Equal(t, "https://example.com/oauth/authorize", config.AuthURL) + assert.Equal(t, "https://example.com/oauth/token", config.TokenURL) + assert.Equal(t, []string{"read", "write"}, config.Scopes) + assert.True(t, config.UsePKCE) + assert.Equal(t, 8080, config.CallbackPort) + assert.Equal(t, map[string]string{"prompt": "select_account", "response_mode": "query"}, config.OAuthParams) }, }, { @@ -222,6 +244,15 @@ func TestCreateOAuthConfigManual(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() + // Prepare OAuth parameters for the specific test case + var oauthParams map[string]string + if tt.name == "valid config with OAuth parameters" { + oauthParams = map[string]string{ + "prompt": "select_account", + "response_mode": "query", + } + } + config, err := CreateOAuthConfigManual( tt.clientID, tt.clientSecret, @@ -230,6 +261,7 @@ func TestCreateOAuthConfigManual(t *testing.T) { tt.scopes, tt.usePKCE, tt.callbackPort, + oauthParams, ) if tt.expectError { @@ -294,6 +326,7 @@ func TestCreateOAuthConfigManual_ScopeDefaultBehavior(t *testing.T) { tt.scopes, true, 8080, + nil, // No OAuth params for basic tests ) require.NoError(t, err) @@ -335,6 +368,7 @@ func TestCreateOAuthConfigManual_PKCEBehavior(t *testing.T) { []string{"read"}, tt.usePKCE, 8080, + nil, // No OAuth params for basic tests ) require.NoError(t, err) @@ -381,6 +415,7 @@ func TestCreateOAuthConfigManual_CallbackPortBehavior(t *testing.T) { []string{"read"}, true, tt.port, + nil, // No OAuth params for basic tests ) require.NoError(t, err) @@ -389,3 +424,67 @@ func TestCreateOAuthConfigManual_CallbackPortBehavior(t *testing.T) { }) } } + +func TestCreateOAuthConfigManual_OAuthParamsBehavior(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + oauthParams map[string]string + expected map[string]string + }{ + { + name: "nil OAuth params", + oauthParams: nil, + expected: nil, + }, + { + name: "empty OAuth params", + oauthParams: map[string]string{}, + expected: map[string]string{}, + }, + { + name: "GitHub-style OAuth params", + oauthParams: map[string]string{ + "prompt": "select_account", + }, + expected: map[string]string{ + "prompt": "select_account", + }, + }, + { + name: "multiple OAuth params", + oauthParams: map[string]string{ + "prompt": "select_account", + "response_mode": "query", + "access_type": "offline", + }, + expected: map[string]string{ + "prompt": "select_account", + "response_mode": "query", + "access_type": "offline", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + config, err := CreateOAuthConfigManual( + "test-client", + "test-secret", + "https://example.com/oauth/authorize", + "https://example.com/oauth/token", + []string{"read"}, + true, + 8080, + tt.oauthParams, + ) + + require.NoError(t, err) + require.NotNil(t, config) + assert.Equal(t, tt.expected, config.OAuthParams) + }) + } +} diff --git a/pkg/config/registry.go b/pkg/config/registry.go index f247599af..726fbd27a 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "fmt" + neturl "net/url" "os" "path/filepath" "strings" @@ -25,7 +26,7 @@ func DetectRegistryType(input string) (registryType string, cleanPath string) { } // Check for HTTP/HTTPS URLs - if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") { + if networking.IsURL(input) { return RegistryTypeURL, input } @@ -35,14 +36,19 @@ func DetectRegistryType(input string) (registryType string, cleanPath string) { // SetRegistryURL validates and sets a registry URL func SetRegistryURL(registryURL string, allowPrivateRegistryIp bool) error { + parsedURL, err := neturl.Parse(registryURL) + if err != nil { + return fmt.Errorf("invalid registry URL: %w", err) + } + if allowPrivateRegistryIp { // we validate either https or http URLs - if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") { + if parsedURL.Scheme != networking.HttpScheme && parsedURL.Scheme != networking.HttpsScheme { return fmt.Errorf("registry URL must start with http:// or https:// when allowing private IPs") } } else { // we just allow https - if !strings.HasPrefix(registryURL, "https://") { + if parsedURL.Scheme != networking.HttpsScheme { return fmt.Errorf("registry URL must start with https:// when not allowing private IPs") } } @@ -59,7 +65,7 @@ func SetRegistryURL(registryURL string, allowPrivateRegistryIp bool) error { } // Update the configuration - err := UpdateConfig(func(c *Config) { + err = UpdateConfig(func(c *Config) { c.RegistryUrl = registryURL c.LocalRegistryPath = "" // Clear local path when setting URL c.AllowPrivateRegistryIp = allowPrivateRegistryIp diff --git a/pkg/networking/http_client.go b/pkg/networking/http_client.go index a97503551..34984ee5a 100644 --- a/pkg/networking/http_client.go +++ b/pkg/networking/http_client.go @@ -20,6 +20,9 @@ var privateIPBlocks []*net.IPNet // HttpTimeout is the timeout for outgoing HTTP requests const HttpTimeout = 30 * time.Second +const HttpsScheme = "https" +const HttpScheme = "http" + // Dialer control function for validating addresses prior to connection func protectedDialerControl(_, address string, _ syscall.RawConn) error { err := AddressReferencesPrivateIp(address) @@ -49,7 +52,7 @@ func (t *ValidatingTransport) RoundTrip(req *http.Request) (*http.Response, erro } // Check for HTTPS scheme - if parsedUrl.Scheme != "https" { + if parsedUrl.Scheme != HttpsScheme { return nil, fmt.Errorf("the supplied URL %s is not HTTPS scheme", req.URL.String()) } diff --git a/pkg/networking/utilities.go b/pkg/networking/utilities.go index 10745c747..7370f2bcf 100644 --- a/pkg/networking/utilities.go +++ b/pkg/networking/utilities.go @@ -88,3 +88,32 @@ func IsLocalhost(host string) bool { host == "127.0.0.1" || host == "[::1]" } + +// IsURL checks if the input is a valid HTTP or HTTPS URL +func IsURL(input string) bool { + parsedURL, err := url.Parse(input) + if err != nil { + return false + } + return parsedURL.Scheme == "http" || parsedURL.Scheme == "https" +} + +// IsRemoteURL checks if the input is a remote HTTP or HTTPS URL (not localhost or private IP) +func IsRemoteURL(input string) bool { + parsedURL, err := url.Parse(input) + if err != nil { + return false + } + + // Must have HTTP or HTTPS scheme + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return false + } + + // Must have a host + if parsedURL.Host == "" { + return false + } + + return !IsLocalhost(parsedURL.Host) +} diff --git a/pkg/networking/utilities_test.go b/pkg/networking/utilities_test.go new file mode 100644 index 000000000..7872496a6 --- /dev/null +++ b/pkg/networking/utilities_test.go @@ -0,0 +1,209 @@ +package networking + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsRemoteURL(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + // Valid remote URLs + { + name: "valid https remote url", + input: "https://api.github.com", + expected: true, + }, + { + name: "valid http remote url", + input: "http://example.com", + expected: true, + }, + { + name: "valid https remote url with path", + input: "https://api.github.com/v3/users", + expected: true, + }, + { + name: "valid https remote url with port", + input: "https://api.github.com:443", + expected: true, + }, + { + name: "valid https remote url with query params", + input: "https://api.github.com/users?page=1&per_page=10", + expected: true, + }, + { + name: "valid https remote url with fragment", + input: "https://api.github.com/users#section", + expected: true, + }, + { + name: "valid https remote url with public IP", + input: "https://8.8.8.8", + expected: true, + }, + { + name: "valid https remote url with public IP and port", + input: "https://8.8.8.8:443", + expected: true, + }, + + // Invalid remote URLs (localhost) + { + name: "localhost without port", + input: "http://localhost", + expected: false, + }, + { + name: "localhost with port", + input: "http://localhost:8080", + expected: false, + }, + { + name: "127.0.0.1 without port", + input: "http://127.0.0.1", + expected: false, + }, + { + name: "127.0.0.1 with port", + input: "http://127.0.0.1:8080", + expected: false, + }, + { + name: "IPv6 localhost", + input: "http://[::1]:8080", + expected: false, + }, + + // Valid remote URLs (private IPs are now considered remote) + { + name: "private IP 10.0.0.1", + input: "http://10.0.0.1", + expected: true, + }, + { + name: "private IP 172.16.0.1", + input: "http://172.16.0.1:8080", + expected: true, + }, + { + name: "private IP 192.168.1.1", + input: "https://192.168.1.1", + expected: true, + }, + { + name: "link-local IPv4", + input: "http://169.254.0.1", + expected: true, + }, + { + name: "link-local IPv6", + input: "http://[fe80::1]", + expected: true, + }, + + // Invalid URLs + { + name: "empty string", + input: "", + expected: false, + }, + { + name: "invalid URL", + input: "not-a-url", + expected: false, + }, + { + name: "missing scheme", + input: "example.com", + expected: false, + }, + { + name: "unsupported scheme", + input: "ftp://example.com", + expected: false, + }, + { + name: "missing host", + input: "https://", + expected: false, + }, + { + name: "missing host with path", + input: "https:///path", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsRemoteURL(tt.input) + assert.Equal(t, tt.expected, result, "Input: %s", tt.input) + }) + } +} + +func TestIsURL(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + // Valid URLs + { + name: "valid https url", + input: "https://api.github.com", + expected: true, + }, + { + name: "valid http url", + input: "http://example.com", + expected: true, + }, + { + name: "valid https url with localhost", + input: "https://localhost:8080", + expected: true, + }, + { + name: "valid http url with private IP", + input: "http://192.168.1.1", + expected: true, + }, + + // Invalid URLs + { + name: "empty string", + input: "", + expected: false, + }, + { + name: "invalid URL", + input: "not-a-url", + expected: false, + }, + { + name: "missing scheme", + input: "example.com", + expected: false, + }, + { + name: "unsupported scheme", + input: "ftp://example.com", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsURL(tt.input) + assert.Equal(t, tt.expected, result, "Input: %s", tt.input) + }) + } +} diff --git a/pkg/registry/provider_local.go b/pkg/registry/provider_local.go index 71d8fb989..2ff6c1d66 100644 --- a/pkg/registry/provider_local.go +++ b/pkg/registry/provider_local.go @@ -67,6 +67,11 @@ func (p *LocalRegistryProvider) GetRegistry() (*Registry, error) { server.Name = name } + // Set name field on each remote server based on map key + for name, server := range registry.RemoteServers { + server.Name = name + } + return registry, nil } diff --git a/pkg/registry/provider_remote.go b/pkg/registry/provider_remote.go index 008dc9eb5..051cb1ae7 100644 --- a/pkg/registry/provider_remote.go +++ b/pkg/registry/provider_remote.go @@ -69,5 +69,10 @@ func (p *RemoteRegistryProvider) GetRegistry() (*Registry, error) { server.Name = name } + // Set name field on each remote server based on map key + for name, server := range registry.RemoteServers { + server.Name = name + } + return registry, nil } diff --git a/pkg/registry/types.go b/pkg/registry/types.go index f78af94ec..46171af59 100644 --- a/pkg/registry/types.go +++ b/pkg/registry/types.go @@ -147,6 +147,12 @@ type OAuthConfig struct { // UsePKCE indicates whether to use PKCE for the OAuth flow // Defaults to true for enhanced security UsePKCE bool `json:"use_pkce,omitempty" yaml:"use_pkce,omitempty"` + // OAuthParams contains additional OAuth parameters to include in the authorization request + // These are server-specific parameters like "prompt", "response_mode", etc. + OAuthParams map[string]string `json:"oauth_params,omitempty" yaml:"oauth_params,omitempty"` + // CallbackPort is the specific port to use for the OAuth callback server + // If not specified, a random available port will be used + CallbackPort int `json:"callback_port,omitempty" yaml:"callback_port,omitempty"` } // RemoteServerMetadata represents the metadata for a remote MCP server accessed via HTTP/HTTPS. diff --git a/pkg/runner/config.go b/pkg/runner/config.go index b077c9821..81142b525 100644 --- a/pkg/runner/config.go +++ b/pkg/runner/config.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "time" "github.com/stacklok/toolhive/pkg/audit" "github.com/stacklok/toolhive/pkg/auth" @@ -39,6 +40,12 @@ type RunConfig struct { // Image is the Docker image to run Image string `json:"image" yaml:"image"` + // RemoteURL is the URL of the remote MCP server (if running remotely) + RemoteURL string `json:"remote_url,omitempty" yaml:"remote_url,omitempty"` + + // RemoteAuthConfig contains OAuth configuration for remote MCP servers + RemoteAuthConfig *RemoteAuthConfig `json:"remote_auth_config,omitempty" yaml:"remote_auth_config,omitempty"` + // CmdArgs are the arguments to pass to the container CmdArgs []string `json:"cmd_args,omitempty" yaml:"cmd_args,omitempty"` @@ -46,10 +53,10 @@ type RunConfig struct { Name string `json:"name" yaml:"name"` // ContainerName is the name of the container - ContainerName string `json:"container_name" yaml:"container_name"` + ContainerName string `json:"container_name,omitempty" yaml:"container_name,omitempty"` // BaseName is the base name used for the container (without prefixes) - BaseName string `json:"base_name" yaml:"base_name"` + BaseName string `json:"base_name,omitempty" yaml:"base_name,omitempty"` // Transport is the transport mode (stdio, sse, or streamable-http) Transport types.TransportType `json:"transport" yaml:"transport"` @@ -356,3 +363,23 @@ func (c *RunConfig) SaveState(ctx context.Context) error { func LoadState(ctx context.Context, name string) (*RunConfig, error) { return state.LoadRunConfig(ctx, name, ReadJSON) } + +// RemoteAuthConfig holds configuration for remote authentication +type RemoteAuthConfig struct { + EnableRemoteAuth bool + ClientID string + ClientSecret string + ClientSecretFile string + Scopes []string + SkipBrowser bool + Timeout time.Duration `swaggertype:"string" example:"5m"` + CallbackPort int + + // OAuth endpoint configuration (from registry) + Issuer string + AuthorizeURL string + TokenURL string + + // OAuth parameters for server-specific customization + OAuthParams map[string]string +} diff --git a/pkg/runner/config_builder.go b/pkg/runner/config_builder.go index 5b2f83ca2..8e87b0d07 100644 --- a/pkg/runner/config_builder.go +++ b/pkg/runner/config_builder.go @@ -53,6 +53,18 @@ func (b *RunConfigBuilder) WithImage(image string) *RunConfigBuilder { return b } +// WithRemoteURL sets the remote URL for the MCP server +func (b *RunConfigBuilder) WithRemoteURL(remoteURL string) *RunConfigBuilder { + b.config.RemoteURL = remoteURL + return b +} + +// WithRemoteAuth sets the remote authentication configuration +func (b *RunConfigBuilder) WithRemoteAuth(config *RemoteAuthConfig) *RunConfigBuilder { + b.config.RemoteAuthConfig = config + return b +} + // WithName sets the MCP server name func (b *RunConfigBuilder) WithName(name string) *RunConfigBuilder { b.config.Name = name diff --git a/pkg/runner/remote_auth.go b/pkg/runner/remote_auth.go new file mode 100644 index 000000000..aae13e149 --- /dev/null +++ b/pkg/runner/remote_auth.go @@ -0,0 +1,81 @@ +package runner + +import ( + "context" + "fmt" + + "golang.org/x/oauth2" + + "github.com/stacklok/toolhive/pkg/auth/discovery" + "github.com/stacklok/toolhive/pkg/logger" +) + +// RemoteAuthHandler handles authentication for remote MCP servers. +// Supports OAuth/OIDC-based authentication with automatic discovery. +type RemoteAuthHandler struct { + config *RemoteAuthConfig +} + +// NewRemoteAuthHandler creates a new remote authentication handler +func NewRemoteAuthHandler(config *RemoteAuthConfig) *RemoteAuthHandler { + return &RemoteAuthHandler{ + config: config, + } +} + +// Authenticate is the main entry point for remote MCP server authentication +func (h *RemoteAuthHandler) Authenticate(ctx context.Context, remoteURL string) (*oauth2.TokenSource, error) { + + // First, try to detect if authentication is required + authInfo, err := discovery.DetectAuthenticationFromServer(ctx, remoteURL, nil) + if err != nil { + logger.Debugf("Could not detect authentication from server: %v", err) + return nil, nil // Not an error, just no auth detected + } + + if authInfo != nil { + logger.Infof("Detected authentication requirement from server - type: %s, realm: %s, resource_metadata: %s", + authInfo.Type, authInfo.Realm, authInfo.ResourceMetadata) + + // Handle OAuth authentication + if authInfo.Type == "OAuth" { + // Use realm as issuer if available, otherwise derive from URL + issuer := authInfo.Realm + if issuer == "" { + issuer = discovery.DeriveIssuerFromURL(remoteURL) + } + + if issuer == "" { + return nil, fmt.Errorf("could not determine OAuth issuer from realm or URL") + } + + logger.Infof("Starting OAuth authentication flow with issuer: %s", issuer) + + // Create OAuth flow config from RemoteAuthConfig + flowConfig := &discovery.OAuthFlowConfig{ + ClientID: h.config.ClientID, + ClientSecret: h.config.ClientSecret, + AuthorizeURL: h.config.AuthorizeURL, + TokenURL: h.config.TokenURL, + Scopes: h.config.Scopes, + CallbackPort: h.config.CallbackPort, + Timeout: h.config.Timeout, + SkipBrowser: h.config.SkipBrowser, + OAuthParams: h.config.OAuthParams, + } + + result, err := discovery.PerformOAuthFlow(ctx, issuer, flowConfig) + if err != nil { + return nil, err + } + + return result.TokenSource, nil + } + + // Currently only OAuth-based authentication is supported + logger.Infof("Unsupported authentication type: %s", authInfo.Type) + return nil, nil + } + + return nil, nil // No authentication required +} diff --git a/pkg/runner/retriever/retriever.go b/pkg/runner/retriever/retriever.go index 615896bea..204539d50 100644 --- a/pkg/runner/retriever/retriever.go +++ b/pkg/runner/retriever/retriever.go @@ -12,6 +12,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/images" "github.com/stacklok/toolhive/pkg/container/verifier" "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/registry" "github.com/stacklok/toolhive/pkg/runner" ) @@ -100,6 +101,41 @@ func GetMCPServer( return imageToUse, imageMetadata, nil } +// GetMCPServerOrRemote retrieves the MCP server definition from the registry, supporting both container and remote servers +func GetMCPServerOrRemote( + ctx context.Context, + serverOrImage string, + rawCACertPath string, + verificationType string, +) (string, *registry.ImageMetadata, *registry.RemoteServerMetadata, error) { + + // First, check if it's a direct URL (existing --remote behavior) + if networking.IsRemoteURL(serverOrImage) { + // Direct URL approach - return as remote server + return serverOrImage, nil, nil, nil + } + + // Second, try to find as a remote server in registry + provider, err := registry.GetDefaultProvider() + if err != nil { + return "", nil, nil, fmt.Errorf("failed to get registry provider: %v", err) + } + + remoteServer, err := provider.GetServer(serverOrImage) + if err == nil { + // Found a remote server in registry + return remoteServer.GetRepositoryURL(), nil, remoteServer.(*registry.RemoteServerMetadata), nil + } + + // Third, try as container server (existing logic) + imageURL, imageMetadata, err := GetMCPServer(ctx, serverOrImage, rawCACertPath, verificationType) + if err != nil { + return "", nil, nil, err + } + + return imageURL, imageMetadata, nil, nil +} + // pullImage pulls an image from a remote registry if it has the "latest" tag // or if it doesn't exist locally. If the image is a local image, it will not be pulled. // If the image has the latest tag, it will be pulled to ensure we have the most recent version. diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index de5ea4bfb..c5bdd51f2 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -10,6 +10,8 @@ import ( "syscall" "time" + "golang.org/x/oauth2" + "github.com/stacklok/toolhive/pkg/client" "github.com/stacklok/toolhive/pkg/config" rt "github.com/stacklok/toolhive/pkg/container/runtime" @@ -158,6 +160,31 @@ func (r *Runner) Run(ctx context.Context) error { // Set up the transport logger.Infof("Setting up %s transport...", r.Config.Transport) + + // For remote MCP servers, set the remote URL on HTTP transports before setup + if r.Config.RemoteURL != "" { + if httpTransport, ok := transportHandler.(interface{ SetRemoteURL(string) }); ok { + httpTransport.SetRemoteURL(r.Config.RemoteURL) + } + + // Handle remote authentication if configured + if r.Config.RemoteAuthConfig != nil && (r.Config.RemoteAuthConfig.EnableRemoteAuth || + r.Config.RemoteAuthConfig.ClientID != "") { + tokenSource, err := r.handleRemoteAuthentication(ctx) + if err != nil { + return fmt.Errorf("failed to authenticate to remote server: %w", err) + } + + // Set the token source on the HTTP transport + if httpTransport, ok := transportHandler.(interface{ SetTokenSource(*oauth2.TokenSource) }); ok { + httpTransport.SetTokenSource(tokenSource) + } + } + + // For remote workloads, we don't need a deployer + r.Config.Deployer = nil + } + if err := transportHandler.Setup( ctx, r.Config.Deployer, r.Config.ContainerName, r.Config.Image, r.Config.CmdArgs, r.Config.EnvVars, r.Config.ContainerLabels, r.Config.PermissionProfile, r.Config.K8sPodTemplatePatch, @@ -284,6 +311,24 @@ func (r *Runner) Run(ctx context.Context) error { return nil } +// handleRemoteAuthentication handles authentication for remote MCP servers +func (r *Runner) handleRemoteAuthentication(ctx context.Context) (*oauth2.TokenSource, error) { + if r.Config.RemoteAuthConfig == nil { + return nil, nil + } + + // Create remote authentication handler + authHandler := NewRemoteAuthHandler(r.Config.RemoteAuthConfig) + + // Perform authentication + tokenSource, err := authHandler.Authenticate(ctx, r.Config.Image) + if err != nil { + return nil, fmt.Errorf("remote authentication failed: %w", err) + } + + return tokenSource, nil +} + // Cleanup performs cleanup operations for the runner, including shutting down all middleware. func (r *Runner) Cleanup(ctx context.Context) error { // For simplicity, return the last error we encounter during cleanup. diff --git a/pkg/transport/http.go b/pkg/transport/http.go index 578851564..08fa732dd 100644 --- a/pkg/transport/http.go +++ b/pkg/transport/http.go @@ -6,6 +6,8 @@ import ( "net/http" "sync" + "golang.org/x/oauth2" + "github.com/stacklok/toolhive/pkg/container" rt "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/ignore" @@ -37,6 +39,12 @@ type HTTPTransport struct { prometheusHandler http.Handler authInfoHandler http.Handler + // Remote MCP server support + remoteURL string + + // tokenSource is the OAuth token source for remote authentication + tokenSource *oauth2.TokenSource + // Mutex for protecting shared state mutex sync.Mutex @@ -88,6 +96,37 @@ func NewHTTPTransport( } } +// SetRemoteURL sets the remote URL for the MCP server +func (t *HTTPTransport) SetRemoteURL(url string) { + t.remoteURL = url +} + +// SetTokenSource sets the OAuth token source for remote authentication +func (t *HTTPTransport) SetTokenSource(tokenSource *oauth2.TokenSource) { + t.tokenSource = tokenSource +} + +// createTokenInjectionMiddleware creates a middleware that injects the OAuth token into requests +func (t *HTTPTransport) createTokenInjectionMiddleware() types.MiddlewareFunction { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if t.tokenSource != nil { + token, err := (*t.tokenSource).Token() + if err != nil { + logger.Warnf("Unable to retrieve OAuth token: %v", err) + // Continue without token rather than failing + } else { + logger.Debugf("Injecting Bearer token into request to %s", r.URL.Path) + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + } + } else { + logger.Debugf("No token source available for request to %s", r.URL.Path) + } + next.ServeHTTP(w, r) + }) + } +} + // Mode returns the transport mode. func (t *HTTPTransport) Mode() types.TransportType { return t.transportType @@ -104,12 +143,29 @@ var transportEnvMap = map[types.TransportType]string{ } // Setup prepares the transport for use. -func (t *HTTPTransport) Setup(ctx context.Context, runtime rt.Deployer, containerName string, image string, cmdArgs []string, - envVars, labels map[string]string, permissionProfile *permissions.Profile, k8sPodTemplatePatch string, - isolateNetwork bool, ignoreConfig *ignore.Config) error { +func (t *HTTPTransport) Setup( + ctx context.Context, + runtime rt.Deployer, + containerName string, + image string, + cmdArgs []string, + envVars map[string]string, + labels map[string]string, + permissionProfile *permissions.Profile, + k8sPodTemplatePatch string, + isolateNetwork bool, + ignoreConfig *ignore.Config, +) error { t.mutex.Lock() defer t.mutex.Unlock() + // For remote MCP servers, we don't need a deployer + if t.remoteURL != "" { + t.containerName = containerName + logger.Infof("Remote transport setup complete for %s -> %s", containerName, t.remoteURL) + return nil + } + t.deployer = runtime t.containerName = containerName @@ -207,38 +263,65 @@ func (t *HTTPTransport) Start(ctx context.Context) error { return errors.ErrContainerNameNotSet } - if t.deployer == nil { + if t.deployer == nil && t.remoteURL == "" { return fmt.Errorf("container deployer not set") } // Create and start the transparent proxy - // The SSE transport forwards requests from the host port to the container's target port - // In a Docker bridge network, we need to use the specified target host - // We ignore containerIP even if it's set, as it's not directly accessible from the host - targetHost := t.targetHost - - // Check if target port is set - if t.targetPort <= 0 { - return fmt.Errorf("target port not set for HTTP transport") + var targetURI string + + if t.remoteURL != "" { + // For remote MCP servers, use the remote URL directly + targetURI = t.remoteURL + logger.Infof("Setting up transparent proxy to forward from host port %d to remote URL %s", + t.proxyPort, targetURI) + } else { + // For local containers, forward to the container's target port + // The SSE transport forwards requests from the host port to the container's target port + // In a Docker bridge network, we need to use the specified target host + // We ignore containerIP even if it's set, as it's not directly accessible from the host + targetHost := t.targetHost + + // Check if target port is set + if t.targetPort <= 0 { + return fmt.Errorf("target port not set for HTTP transport") + } + + // Use the target port for the container + containerPort := t.targetPort + targetURI = fmt.Sprintf("http://%s:%d", targetHost, containerPort) + logger.Infof("Setting up transparent proxy to forward from host port %d to %s", + t.proxyPort, targetURI) } - // Use the target port for the container - containerPort := t.targetPort - targetURI := fmt.Sprintf("http://%s:%d", targetHost, containerPort) - logger.Infof("Setting up transparent proxy to forward from host port %d to %s", - t.proxyPort, targetURI) + // Create middlewares slice + var middlewares []types.MiddlewareFunction + + // Add the transport's existing middlewares + middlewares = append(middlewares, t.middlewares...) + + // Add OAuth token injection middleware for remote authentication if we have a token source + if t.remoteURL != "" && t.tokenSource != nil { + tokenMiddleware := t.createTokenInjectionMiddleware() + middlewares = append(middlewares, tokenMiddleware) + } - // Create the transparent proxy with middlewares + // Create the transparent proxy t.proxy = transparent.NewTransparentProxy( t.host, t.proxyPort, t.containerName, targetURI, t.prometheusHandler, t.authInfoHandler, - true, - t.middlewares...) + t.remoteURL == "", + middlewares...) if err := t.proxy.Start(ctx); err != nil { return err } - logger.Infof("HTTP transport started for container %s on port %d", t.containerName, t.proxyPort) + logger.Infof("HTTP transport started for %s on port %d", t.containerName, t.proxyPort) + + // For remote MCP servers, we don't need container monitoring + if t.remoteURL != "" { + return nil + } // Create a container monitor monitorRuntime, err := container.NewFactory().Create(ctx) @@ -267,10 +350,20 @@ func (t *HTTPTransport) Stop(ctx context.Context) error { // Signal shutdown close(t.shutdownCh) - // Stop the monitor if it's running - if t.monitor != nil { - t.monitor.StopMonitoring() - t.monitor = nil + // For remote MCP servers, we don't need container monitoring + if t.remoteURL == "" { + // Stop the monitor if it's running + if t.monitor != nil { + t.monitor.StopMonitoring() + t.monitor = nil + } + + // Stop the container if deployer is available + if t.deployer != nil && t.containerName != "" { + if err := t.deployer.StopWorkload(ctx, t.containerName); err != nil { + return fmt.Errorf("failed to stop workload: %w", err) + } + } } // Stop the transparent proxy @@ -280,13 +373,6 @@ func (t *HTTPTransport) Stop(ctx context.Context) error { } } - // Stop the container if deployer is available - if t.deployer != nil && t.containerName != "" { - if err := t.deployer.StopWorkload(ctx, t.containerName); err != nil { - return fmt.Errorf("failed to stop workload: %w", err) - } - } - return nil } diff --git a/pkg/transport/proxy/transparent/transparent_proxy.go b/pkg/transport/proxy/transparent/transparent_proxy.go index 2b043dc17..8ccd9ecec 100644 --- a/pkg/transport/proxy/transparent/transparent_proxy.go +++ b/pkg/transport/proxy/transparent/transparent_proxy.go @@ -24,6 +24,7 @@ import ( "github.com/stacklok/toolhive/pkg/healthcheck" "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/transport/session" "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -125,7 +126,52 @@ func (t *tracingTransport) forward(req *http.Request) (*http.Response, error) { return tr.RoundTrip(req) } +// manualForward manually forwards a request to the remote server using an HTTP client +func (t *tracingTransport) manualForward(req *http.Request) (*http.Response, error) { + // Create a new request to the target URL + targetURL := t.p.targetURI + req.URL.Path + if req.URL.RawQuery != "" { + targetURL += "?" + req.URL.RawQuery + } + + newReq, err := http.NewRequest(req.Method, targetURL, req.Body) + if err != nil { + return nil, fmt.Errorf("failed to create new request: %w", err) + } + + // Copy headers from the original request + for name, values := range req.Header { + for _, value := range values { + newReq.Header.Add(name, value) + } + } + + // Create HTTP client and make the request + client := &http.Client{ + Timeout: 30 * time.Second, + } + + return client.Do(newReq) +} + +// nolint:gocyclo // This function handles multiple request types and is complex by design func (t *tracingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if networking.IsRemoteURL(t.p.targetURI) { + // Use manual HTTP client instead of reverse proxy for remote URLs + resp, err := t.manualForward(req) + if err != nil { + if errors.Is(err, context.Canceled) { + // Expected during shutdown or client disconnect—silently ignore + return nil, err + } + logger.Errorf("Failed to forward request: %v", err) + return nil, err + } + + return resp, nil + } + + // Original logic for local containers reqBody := readRequestBody(req) path := req.URL.Path @@ -245,6 +291,7 @@ func (p *TransparentProxy) modifyForSessionID(resp *http.Response) error { } // Start starts the transparent proxy. +// nolint:gocyclo // This function handles multiple startup scenarios and is complex by design func (p *TransparentProxy) Start(ctx context.Context) error { p.mutex.Lock() defer p.mutex.Unlock() @@ -263,9 +310,19 @@ func (p *TransparentProxy) Start(ctx context.Context) error { return p.modifyForSessionID(resp) } - // Create a handler that logs requests + // Create a handler that logs requests and strips /mcp path for remote servers handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger.Infof("Transparent proxy: %s %s -> %s", r.Method, r.URL.Path, targetURL) + // For remote servers, strip the /mcp path since they expect requests at the root + if strings.HasPrefix(r.URL.Path, "/mcp") { + // Strip /mcp from the path for remote servers + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/mcp") + if r.URL.Path == "" { + r.URL.Path = "/" + } + logger.Infof("Transparent proxy: %s %s -> %s (stripped /mcp)", r.Method, r.URL.Path, targetURL) + } else { + logger.Infof("Transparent proxy: %s %s -> %s", r.Method, r.URL.Path, targetURL) + } proxy.ServeHTTP(w, r) }) diff --git a/pkg/transport/proxy/transparent/transparent_test.go b/pkg/transport/proxy/transparent/transparent_test.go index 409cd01cf..8d8bd78e8 100644 --- a/pkg/transport/proxy/transparent/transparent_test.go +++ b/pkg/transport/proxy/transparent/transparent_test.go @@ -20,7 +20,7 @@ func init() { func TestStreamingSessionIDDetection(t *testing.T) { t.Parallel() - proxy := NewTransparentProxy("127.0.0.1", 0, "test", "http://example.com", nil, nil, true) + proxy := NewTransparentProxy("127.0.0.1", 0, "test", "", nil, nil, true) target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") w.WriteHeader(200) diff --git a/pkg/workloads/manager.go b/pkg/workloads/manager.go index fb2786b24..d9e8bdc7a 100644 --- a/pkg/workloads/manager.go +++ b/pkg/workloads/manager.go @@ -135,7 +135,22 @@ func (d *defaultManager) DoesWorkloadExist(ctx context.Context, workloadName str func (d *defaultManager) ListWorkloads(ctx context.Context, listAll bool, labelFilters ...string) ([]core.Workload, error) { // For the sake of minimizing changes, delegate to the status manager. // Whether this method should still belong to the workload manager is TBD. - return d.statuses.ListWorkloads(ctx, listAll, labelFilters) + containerWorkloads, err := d.statuses.ListWorkloads(ctx, listAll, labelFilters) + if err != nil { + return nil, err + } + + // Get remote workloads from the state store + remoteWorkloads, err := d.getRemoteWorkloadsFromState(ctx, listAll, labelFilters) + if err != nil { + logger.Warnf("Failed to get remote workloads from state: %v", err) + // Continue with container workloads only + } else { + // Combine container and remote workloads + containerWorkloads = append(containerWorkloads, remoteWorkloads...) + } + + return containerWorkloads, nil } func (d *defaultManager) StopWorkloads(ctx context.Context, names []string) (*errgroup.Group, error) { @@ -622,8 +637,13 @@ func (d *defaultManager) loadRunnerFromState(ctx context.Context, baseName strin return nil, err } - // Update the runtime in the loaded configuration - runConfig.Deployer = d.runtime + if runConfig.RemoteURL != "" { + // For remote workloads, we don't need a deployer + runConfig.Deployer = nil + } else { + // Update the runtime in the loaded configuration + runConfig.Deployer = d.runtime + } // Create a new runner with the loaded configuration return runner.NewRunner(runConfig, d.statuses), nil @@ -751,3 +771,74 @@ func (d *defaultManager) ListWorkloadsInGroup(ctx context.Context, groupName str return groupWorkloads, nil } + +// getRemoteWorkloadsFromState retrieves remote servers from the state store +func (*defaultManager) getRemoteWorkloadsFromState(ctx context.Context, _ bool, labelFilters []string) ([]core.Workload, error) { + // Create a state store + store, err := state.NewRunConfigStore(state.DefaultAppName) + if err != nil { + return nil, fmt.Errorf("failed to create state store: %w", err) + } + + // List all configurations + configNames, err := store.List(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list configurations: %w", err) + } + + // Parse the filters into a format we can use for matching + parsedFilters, err := types.ParseLabelFilters(labelFilters) + if err != nil { + return nil, fmt.Errorf("failed to parse label filters: %v", err) + } + + var remoteWorkloads []core.Workload + + for _, name := range configNames { + // Load the run configuration + reader, err := store.GetReader(ctx, name) + if err != nil { + logger.Warnf("failed to read configuration for %s: %v", name, err) + continue + } + + // Parse the run configuration + runConfig, err := runner.ReadJSON(reader) + if closeErr := reader.Close(); closeErr != nil { + logger.Warnf("failed to close reader for %s: %v", name, closeErr) + } + if err != nil { + logger.Warnf("failed to parse configuration for %s: %v", name, err) + continue + } + + // Only include remote servers (those with RemoteURL set) + if runConfig.RemoteURL == "" { + continue + } + + // Use the transport type directly since it's already parsed + transportType := runConfig.Transport + + // Create a workload from the run configuration + workload := core.Workload{ + Name: name, + Package: "remote", + Status: rt.WorkloadStatusRunning, // Remote servers are always considered running + URL: runConfig.RemoteURL, + Port: 0, // Remote servers don't have a local port + TransportType: transportType, + ToolType: "remote", + Group: runConfig.Group, + CreatedAt: time.Now(), // Use current time since RunConfig doesn't store creation time + Labels: runConfig.ContainerLabels, + } + + // Apply label filtering + if types.MatchesLabelFilters(workload.Labels, parsedFilters) { + remoteWorkloads = append(remoteWorkloads, workload) + } + } + + return remoteWorkloads, nil +}