diff --git a/cmd/thv/app/list.go b/cmd/thv/app/list.go index 23613173e..90b076dae 100644 --- a/cmd/thv/app/list.go +++ b/cmd/thv/app/list.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + rt "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/core" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/workloads" @@ -139,11 +140,17 @@ func printTextOutput(workloadList []core.Workload) { // Print workload information for _, c := range workloadList { + // Highlight unauthenticated workloads with a warning indicator + status := string(c.Status) + if c.Status == rt.WorkloadStatusUnauthenticated { + status = "⚠️ " + status + } + // Print workload information fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n", c.Name, c.Package, - c.Status, + status, c.URL, c.Port, c.ToolType, diff --git a/cmd/thv/app/proxy.go b/cmd/thv/app/proxy.go index c2438fa0b..7ce6c6ecd 100644 --- a/cmd/thv/app/proxy.go +++ b/cmd/thv/app/proxy.go @@ -3,7 +3,6 @@ package app import ( "context" "fmt" - "net/http" "net/url" "os/signal" "syscall" @@ -19,6 +18,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/transport" + "github.com/stacklok/toolhive/pkg/transport/middleware" "github.com/stacklok/toolhive/pkg/transport/proxy/transparent" "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -375,18 +375,7 @@ func resolveClientSecret() (string, error) { // createTokenInjectionMiddleware creates a middleware that injects the OAuth token into requests func createTokenInjectionMiddleware(tokenSource oauth2.TokenSource) types.MiddlewareFunction { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token, err := tokenSource.Token() - if err != nil { - http.Error(w, "Unable to retrieve OAuth token", http.StatusUnauthorized) - return - } - - r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) - next.ServeHTTP(w, r) - }) - } + return middleware.CreateTokenInjectionMiddleware(tokenSource) } // addExternalTokenMiddleware adds token exchange or token injection middleware to the middleware chain diff --git a/docs/arch/02-core-concepts.md b/docs/arch/02-core-concepts.md index c31d434f2..e4f28df95 100644 --- a/docs/arch/02-core-concepts.md +++ b/docs/arch/02-core-concepts.md @@ -37,6 +37,7 @@ A **workload** is the fundamental deployment unit in ToolHive. It represents eve - `removing` - Workload is being deleted - `error` - Workload encountered an error - `unhealthy` - Workload is running but unhealthy +- `unauthenticated` - Remote workload cannot authenticate (expired tokens) **Implementation:** - Interface: `pkg/workloads/manager.go` diff --git a/docs/arch/08-workloads-lifecycle.md b/docs/arch/08-workloads-lifecycle.md index f0454b140..3fbf893b0 100644 --- a/docs/arch/08-workloads-lifecycle.md +++ b/docs/arch/08-workloads-lifecycle.md @@ -21,12 +21,16 @@ stateDiagram-v2 Running --> Stopping: Stop Running --> Unhealthy: Health Failed + Running --> Unauthenticated: Auth Failed Running --> Stopped: Container Exit Stopping --> Stopped: Success Stopped --> Starting: Restart Stopped --> Removing: Delete + Unauthenticated --> Starting: Re-authenticate + Unauthenticated --> Removing: Delete + Removing --> [*]: Success Error --> Starting: Restart Error --> Removing: Delete @@ -34,7 +38,7 @@ stateDiagram-v2 **States**: `pkg/container/runtime/types.go` - `starting`, `running`, `stopping`, `stopped` -- `removing`, `error`, `unhealthy` +- `removing`, `error`, `unhealthy`, `unauthenticated` ## Core Operations diff --git a/docs/server/docs.go b/docs/server/docs.go index 13f1896b3..3a074a59c 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"},"insecureAllowHTTP":{"description":"InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing\nWARNING: This is insecure and should NEVER be used in production","type":"boolean"},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]},"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"},"proxy_mode":{"description":"ProxyMode is the proxy mode that clients should use to connect.\nFor stdio transports, this will be the proxy mode (sse or streamable-http).\nFor direct transports (sse/streamable-http), this will be the same as TransportType.","type":"string"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"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.InboundNetworkPermissions":{"description":"Inbound defines inbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts for inbound connections","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"inbound":{"$ref":"#/components/schemas/permissions.InboundNetworkPermissions"},"mode":{"description":"Mode specifies the network mode for the container (e.g., \"host\", \"bridge\", \"none\")\nWhen empty, the default container runtime network mode is used","type":"string"},"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.Group":{"properties":{"description":{"description":"Description is a human-readable description of the group's purpose and functionality","type":"string"},"name":{"description":"Name is the identifier for the group, used when referencing the group in commands","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions within this group","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"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":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/registry.Group"},"type":"array","uniqueItems":false},"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"},"remote.Config":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorize_url":{"type":"string"},"callback_port":{"type":"integer"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_secret_file":{"type":"string"},"env_vars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"token_url":{"type":"string"},"use_pkce":{"type":"boolean"}},"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_file_dir":{"description":"EnvFileDir is the directory path to load environment files from","type":"string"},"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/remote.Config"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolsOverride is a map from an actual tool to its overridden name and/or description","type":"object"},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"trust_proxy_headers":{"description":"TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"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"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"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":{"customAttributes":{"additionalProperties":{"type":"string"},"description":"CustomAttributes contains custom resource attributes to be added to all telemetry signals.\nThese are parsed from CLI flags (--otel-custom-attributes) or environment variables\n(OTEL_RESOURCE_ATTRIBUTES) as key=value pairs.\nWe use map[string]string for proper JSON serialization instead of []attribute.KeyValue\nwhich doesn't marshal/unmarshal correctly.","type":"object"},"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","uniqueItems":false},"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"},"metricsEnabled":{"description":"MetricsEnabled controls whether OTLP metrics are enabled\nWhen false, OTLP metrics are not sent even if an endpoint is configured\nThis is independent of EnablePrometheusMetricsPath","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)\nOnly used when TracingEnabled is true","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"},"tracingEnabled":{"description":"TracingEnabled controls whether distributed tracing is enabled\nWhen false, no tracer provider is created even if an endpoint is configured","type":"boolean"}},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]}},"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","LMStudio","Goose","Trae","Continue"]}},"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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"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.toolOverride":{"description":"Tool override","properties":{"description":{"description":"Description of the tool","type":"string"},"name":{"description":"Name of the tool","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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"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"},"insecureAllowHTTP":{"description":"InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing\nWARNING: This is insecure and should NEVER be used in production","type":"boolean"},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]},"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"},"proxy_mode":{"description":"ProxyMode is the proxy mode that clients should use to connect.\nFor stdio transports, this will be the proxy mode (sse or streamable-http).\nFor direct transports (sse/streamable-http), this will be the same as TransportType.","type":"string"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"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.InboundNetworkPermissions":{"description":"Inbound defines inbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts for inbound connections","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"inbound":{"$ref":"#/components/schemas/permissions.InboundNetworkPermissions"},"mode":{"description":"Mode specifies the network mode for the container (e.g., \"host\", \"bridge\", \"none\")\nWhen empty, the default container runtime network mode is used","type":"string"},"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.Group":{"properties":{"description":{"description":"Description is a human-readable description of the group's purpose and functionality","type":"string"},"name":{"description":"Name is the identifier for the group, used when referencing the group in commands","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions within this group","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"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":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/registry.Group"},"type":"array","uniqueItems":false},"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"},"remote.Config":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorize_url":{"type":"string"},"callback_port":{"type":"integer"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_secret_file":{"type":"string"},"env_vars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"token_url":{"type":"string"},"use_pkce":{"type":"boolean"}},"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_file_dir":{"description":"EnvFileDir is the directory path to load environment files from","type":"string"},"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/remote.Config"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolsOverride is a map from an actual tool to its overridden name and/or description","type":"object"},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"trust_proxy_headers":{"description":"TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"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"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"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","WorkloadStatusUnauthenticated"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"customAttributes":{"additionalProperties":{"type":"string"},"description":"CustomAttributes contains custom resource attributes to be added to all telemetry signals.\nThese are parsed from CLI flags (--otel-custom-attributes) or environment variables\n(OTEL_RESOURCE_ATTRIBUTES) as key=value pairs.\nWe use map[string]string for proper JSON serialization instead of []attribute.KeyValue\nwhich doesn't marshal/unmarshal correctly.","type":"object"},"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","uniqueItems":false},"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"},"metricsEnabled":{"description":"MetricsEnabled controls whether OTLP metrics are enabled\nWhen false, OTLP metrics are not sent even if an endpoint is configured\nThis is independent of EnablePrometheusMetricsPath","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)\nOnly used when TracingEnabled is true","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"},"tracingEnabled":{"description":"TracingEnabled controls whether distributed tracing is enabled\nWhen false, no tracer provider is created even if an endpoint is configured","type":"boolean"}},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]}},"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","LMStudio","Goose","Trae","Continue"]}},"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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"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.toolOverride":{"description":"Tool override","properties":{"description":{"description":"Description of the tool","type":"string"},"name":{"description":"Name of the tool","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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown","WorkloadStatusUnauthenticated"]}},"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"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid workload name"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/proxy-logs":{"get":{"description":"Retrieve proxy logs for a specific workload by name from the file system.","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":"Proxy logs for the specified workload"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid workload name"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Proxy logs not found for workload"}},"summary":"Get proxy 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}/status":{"get":{"description":"Get the current status 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.workloadStatusResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload status","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 54ab330a0..a8212d41e 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"},"insecureAllowHTTP":{"description":"InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing\nWARNING: This is insecure and should NEVER be used in production","type":"boolean"},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]},"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"},"proxy_mode":{"description":"ProxyMode is the proxy mode that clients should use to connect.\nFor stdio transports, this will be the proxy mode (sse or streamable-http).\nFor direct transports (sse/streamable-http), this will be the same as TransportType.","type":"string"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"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.InboundNetworkPermissions":{"description":"Inbound defines inbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts for inbound connections","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"inbound":{"$ref":"#/components/schemas/permissions.InboundNetworkPermissions"},"mode":{"description":"Mode specifies the network mode for the container (e.g., \"host\", \"bridge\", \"none\")\nWhen empty, the default container runtime network mode is used","type":"string"},"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.Group":{"properties":{"description":{"description":"Description is a human-readable description of the group's purpose and functionality","type":"string"},"name":{"description":"Name is the identifier for the group, used when referencing the group in commands","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions within this group","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"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":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/registry.Group"},"type":"array","uniqueItems":false},"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"},"remote.Config":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorize_url":{"type":"string"},"callback_port":{"type":"integer"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_secret_file":{"type":"string"},"env_vars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"token_url":{"type":"string"},"use_pkce":{"type":"boolean"}},"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_file_dir":{"description":"EnvFileDir is the directory path to load environment files from","type":"string"},"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/remote.Config"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolsOverride is a map from an actual tool to its overridden name and/or description","type":"object"},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"trust_proxy_headers":{"description":"TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"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"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"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":{"customAttributes":{"additionalProperties":{"type":"string"},"description":"CustomAttributes contains custom resource attributes to be added to all telemetry signals.\nThese are parsed from CLI flags (--otel-custom-attributes) or environment variables\n(OTEL_RESOURCE_ATTRIBUTES) as key=value pairs.\nWe use map[string]string for proper JSON serialization instead of []attribute.KeyValue\nwhich doesn't marshal/unmarshal correctly.","type":"object"},"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","uniqueItems":false},"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"},"metricsEnabled":{"description":"MetricsEnabled controls whether OTLP metrics are enabled\nWhen false, OTLP metrics are not sent even if an endpoint is configured\nThis is independent of EnablePrometheusMetricsPath","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)\nOnly used when TracingEnabled is true","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"},"tracingEnabled":{"description":"TracingEnabled controls whether distributed tracing is enabled\nWhen false, no tracer provider is created even if an endpoint is configured","type":"boolean"}},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]}},"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","LMStudio","Goose","Trae","Continue"]}},"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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"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.toolOverride":{"description":"Tool override","properties":{"description":{"description":"Description of the tool","type":"string"},"name":{"description":"Name of the tool","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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"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"},"insecureAllowHTTP":{"description":"InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing\nWARNING: This is insecure and should NEVER be used in production","type":"boolean"},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]},"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"},"proxy_mode":{"description":"ProxyMode is the proxy mode that clients should use to connect.\nFor stdio transports, this will be the proxy mode (sse or streamable-http).\nFor direct transports (sse/streamable-http), this will be the same as TransportType.","type":"string"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"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.InboundNetworkPermissions":{"description":"Inbound defines inbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts for inbound connections","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"inbound":{"$ref":"#/components/schemas/permissions.InboundNetworkPermissions"},"mode":{"description":"Mode specifies the network mode for the container (e.g., \"host\", \"bridge\", \"none\")\nWhen empty, the default container runtime network mode is used","type":"string"},"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.Group":{"properties":{"description":{"description":"Description is a human-readable description of the group's purpose and functionality","type":"string"},"name":{"description":"Name is the identifier for the group, used when referencing the group in commands","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions within this group","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"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":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/registry.Group"},"type":"array","uniqueItems":false},"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"},"remote.Config":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorize_url":{"type":"string"},"callback_port":{"type":"integer"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_secret_file":{"type":"string"},"env_vars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"token_url":{"type":"string"},"use_pkce":{"type":"boolean"}},"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_file_dir":{"description":"EnvFileDir is the directory path to load environment files from","type":"string"},"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/remote.Config"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolsOverride is a map from an actual tool to its overridden name and/or description","type":"object"},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"trust_proxy_headers":{"description":"TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"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"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"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","WorkloadStatusUnauthenticated"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"customAttributes":{"additionalProperties":{"type":"string"},"description":"CustomAttributes contains custom resource attributes to be added to all telemetry signals.\nThese are parsed from CLI flags (--otel-custom-attributes) or environment variables\n(OTEL_RESOURCE_ATTRIBUTES) as key=value pairs.\nWe use map[string]string for proper JSON serialization instead of []attribute.KeyValue\nwhich doesn't marshal/unmarshal correctly.","type":"object"},"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","uniqueItems":false},"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"},"metricsEnabled":{"description":"MetricsEnabled controls whether OTLP metrics are enabled\nWhen false, OTLP metrics are not sent even if an endpoint is configured\nThis is independent of EnablePrometheusMetricsPath","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)\nOnly used when TracingEnabled is true","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"},"tracingEnabled":{"description":"TracingEnabled controls whether distributed tracing is enabled\nWhen false, no tracer provider is created even if an endpoint is configured","type":"boolean"}},"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","LMStudio","Goose","Trae","Continue"]},"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","LMStudio","Goose","Trae","Continue"]}},"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","LMStudio","Goose","Trae","Continue"]}},"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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"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.toolOverride":{"description":"Tool override","properties":{"description":{"description":"Description of the tool","type":"string"},"name":{"description":"Name of the tool","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"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"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"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"proxy_port":{"description":"Port for the HTTP proxy to listen on","type":"integer"},"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},"tools_override":{"additionalProperties":{"$ref":"#/components/schemas/v1.toolOverride"},"description":"Tools override","type":"object"},"transport":{"description":"Transport configuration","type":"string"},"trust_proxy_headers":{"description":"Whether to trust X-Forwarded-* headers from reverse proxies","type":"boolean"},"url":{"description":"Remote server specific fields","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"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown","WorkloadStatusUnauthenticated"]}},"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"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid workload name"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/proxy-logs":{"get":{"description":"Retrieve proxy logs for a specific workload by name from the file system.","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":"Proxy logs for the specified workload"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid workload name"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Proxy logs not found for workload"}},"summary":"Get proxy 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}/status":{"get":{"description":"Get the current status 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.workloadStatusResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload status","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 16d345611..bbffe9e54 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -910,6 +910,7 @@ components: - WorkloadStatusUnhealthy - WorkloadStatusRemoving - WorkloadStatusUnknown + - WorkloadStatusUnauthenticated secrets.SecretParameter: properties: name: @@ -1650,6 +1651,7 @@ components: - WorkloadStatusUnhealthy - WorkloadStatusRemoving - WorkloadStatusUnknown + - WorkloadStatusUnauthenticated type: object externalDocs: description: "" diff --git a/pkg/auth/monitored_token_source.go b/pkg/auth/monitored_token_source.go new file mode 100644 index 000000000..76350c456 --- /dev/null +++ b/pkg/auth/monitored_token_source.go @@ -0,0 +1,136 @@ +package auth + +import ( + "context" + "fmt" + "sync" + "time" + + "golang.org/x/oauth2" + + "github.com/stacklok/toolhive/pkg/container/runtime" +) + +// StatusUpdater is an interface for updating workload authentication status. +// This abstraction allows the monitored token source to work with any status management system +// without creating import cycles. +type StatusUpdater interface { + SetWorkloadStatus(ctx context.Context, workloadName string, status runtime.WorkloadStatus, reason string) error +} + +// MonitoredTokenSource is a wrapper around an oauth2.TokenSource that monitors authentication +// failures and automatically marks workloads as unauthenticated when tokens expire or fail. +// It provides both per-request token retrieval and background monitoring. +type MonitoredTokenSource struct { + tokenSource oauth2.TokenSource + workloadName string + statusUpdater StatusUpdater + monitoringCtx context.Context + stopMonitoring chan struct{} + stopOnce sync.Once + + timer *time.Timer +} + +// NewMonitoredTokenSource creates a new MonitoredTokenSource that wraps the provided +// oauth2.TokenSource and monitors it for authentication failures. +func NewMonitoredTokenSource( + ctx context.Context, + tokenSource oauth2.TokenSource, + workloadName string, + statusUpdater StatusUpdater, +) *MonitoredTokenSource { + return &MonitoredTokenSource{ + tokenSource: tokenSource, + workloadName: workloadName, + statusUpdater: statusUpdater, + monitoringCtx: ctx, + stopMonitoring: make(chan struct{}), + } +} + +// Token retrieves a token from the token source and will mark the workload as unauthenticated +// if the token retrieval fails. +func (mts *MonitoredTokenSource) Token() (*oauth2.Token, error) { + tok, err := mts.tokenSource.Token() + if err != nil { + mts.markAsUnauthenticated(fmt.Sprintf("Token retrieval failed: %v", err)) + return nil, err + } + return tok, nil +} + +// StartBackgroundMonitoring starts the background monitoring goroutine that checks +// token validity at expiry time and marks the workload as unauthenticated on the failure. +func (mts *MonitoredTokenSource) StartBackgroundMonitoring() { + if mts.timer == nil { + mts.timer = time.NewTimer(time.Millisecond) // kick immediately + } + go mts.monitorLoop() +} + +func (mts *MonitoredTokenSource) monitorLoop() { + for { + select { + case <-mts.monitoringCtx.Done(): + mts.stopTimer() + return + case <-mts.stopMonitoring: + mts.stopTimer() + return + case <-mts.timer.C: + shouldStop, next := mts.onTick() + if shouldStop { + mts.stopTimer() + return + } + mts.resetTimer(next) + } + } +} + +func (mts *MonitoredTokenSource) stopTimer() { + if mts.timer != nil && !mts.timer.Stop() { + select { + case <-mts.timer.C: + default: + } + } +} + +func (mts *MonitoredTokenSource) resetTimer(d time.Duration) { + mts.stopTimer() + mts.timer.Reset(d) +} + +// onTick returns (shouldStop bool, nextDelay time.Duration) +func (mts *MonitoredTokenSource) onTick() (bool, time.Duration) { + tok, err := mts.tokenSource.Token() + if err != nil { + // Any error → mark as unauthenticated and stop + mts.markAsUnauthenticated(fmt.Sprintf("No valid token: %v", err)) + return true, 0 + } + + // Success → schedule next check + if tok.Expiry.IsZero() { + // no expiry → nothing to monitor + return true, 0 + } + wait := time.Until(tok.Expiry) + if wait < time.Second { + wait = time.Second + } + return false, wait +} + +// markAsUnauthenticated marks the workload as unauthenticated. +func (mts *MonitoredTokenSource) markAsUnauthenticated(reason string) { + _ = mts.statusUpdater.SetWorkloadStatus( + context.Background(), + mts.workloadName, + runtime.WorkloadStatusUnauthenticated, + reason, + ) + mts.stopOnce.Do(func() { close(mts.stopMonitoring) }) +} diff --git a/pkg/auth/monitored_token_source_test.go b/pkg/auth/monitored_token_source_test.go new file mode 100644 index 000000000..271275635 --- /dev/null +++ b/pkg/auth/monitored_token_source_test.go @@ -0,0 +1,404 @@ +package auth + +import ( + "context" + "errors" + "net/http" + "strings" + "sync" + "testing" + "time" + + "go.uber.org/mock/gomock" + "golang.org/x/oauth2" + + rt "github.com/stacklok/toolhive/pkg/container/runtime" + statusMocks "github.com/stacklok/toolhive/pkg/workloads/statuses/mocks" +) + +// mockStatusUpdater adapts a mock statuses.StatusManager to auth.StatusUpdater for testing +type mockStatusUpdater struct { + sm *statusMocks.MockStatusManager +} + +func newMockStatusUpdater(ctrl *gomock.Controller) (*mockStatusUpdater, *statusMocks.MockStatusManager) { + mockSM := statusMocks.NewMockStatusManager(ctrl) + return &mockStatusUpdater{sm: mockSM}, mockSM +} + +func (m *mockStatusUpdater) SetWorkloadStatus(ctx context.Context, workloadName string, status rt.WorkloadStatus, reason string) error { + return m.sm.SetWorkloadStatus(ctx, workloadName, status, reason) +} + +// mockTokenSource is a simple mock implementation of oauth2.TokenSource for testing. +// It uses a callback function to allow flexible token/error configuration. +type mockTokenSource struct { + mu sync.Mutex + tokenFn func() (*oauth2.Token, error) + callCount int +} + +func newMockTokenSource() *mockTokenSource { + return &mockTokenSource{ + tokenFn: func() (*oauth2.Token, error) { + return nil, errors.New("no token configured") + }, + } +} + +func (m *mockTokenSource) setTokenFn(fn func() (*oauth2.Token, error)) { + m.mu.Lock() + defer m.mu.Unlock() + m.tokenFn = fn +} + +func (m *mockTokenSource) Token() (*oauth2.Token, error) { + m.mu.Lock() + defer m.mu.Unlock() + m.callCount++ + return m.tokenFn() +} + +// createRetrieveError creates an error for testing token failures +func createRetrieveError(statusCode int, body string) *oauth2.RetrieveError { + response := &http.Response{ + StatusCode: statusCode, + Body: http.NoBody, + } + return &oauth2.RetrieveError{ + Response: response, + Body: []byte(body), + } +} + +func TestMonitoredTokenSource_SuccessfulTokenRetrieval(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, _ := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + validToken := &oauth2.Token{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + Expiry: time.Now().Add(time.Hour), + } + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return validToken, nil + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Test successful token retrieval + token, err := ats.Token() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if token.AccessToken != "test-access-token" { + t.Errorf("Expected access token 'test-access-token', got %s", token.AccessToken) + } + + // Should not have called SetWorkloadStatus for successful retrieval + // (no expectations set means we expect it not to be called) +} + +func TestMonitoredTokenSource_AuthenticationErrorMarksUnauthenticated(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, statusManager := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + // Create an error that simulates token retrieval failure + retrieveErr := createRetrieveError(http.StatusBadRequest, `{"error":"invalid_grant","error_description":"refresh token expired"}`) + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return nil, retrieveErr + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Expect SetWorkloadStatus to be called with unauthenticated status + statusManager.EXPECT(). + SetWorkloadStatus( + gomock.Any(), + "test-workload", + rt.WorkloadStatusUnauthenticated, + gomock.Any(), + ). + DoAndReturn(func(_ context.Context, _ string, _ rt.WorkloadStatus, reason string) error { + if !strings.Contains(reason, "invalid_grant") { + t.Errorf("Expected reason to contain 'invalid_grant', got %s", reason) + } + return nil + }). + Times(1) + + // Token retrieval should fail and mark as unauthenticated + _, err := ats.Token() + if err == nil { + t.Fatal("Expected error, got nil") + } + + // Give a moment for the async call to complete + time.Sleep(50 * time.Millisecond) +} + +func TestMonitoredTokenSource_ErrorMarksUnauthenticated(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, statusManager := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + // Any error should mark as unauthenticated + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return nil, errors.New("some generic error") + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Expect SetWorkloadStatus to be called for any error + statusManager.EXPECT(). + SetWorkloadStatus( + gomock.Any(), + "test-workload", + rt.WorkloadStatusUnauthenticated, + gomock.Any(), + ). + Return(nil). + Times(1) + + // Token retrieval should fail and mark as unauthenticated + _, err := ats.Token() + if err == nil { + t.Fatal("Expected error, got nil") + } + + // Give a moment for the async call to complete + time.Sleep(50 * time.Millisecond) +} + +func TestMonitoredTokenSource_BackgroundMonitoring(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, statusManager := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + callCount := 0 + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + callCount++ + if callCount == 1 { + // First call: return valid token with short expiry + return &oauth2.Token{ + AccessToken: "test-token", + RefreshToken: "test-refresh", + Expiry: time.Now().Add(500 * time.Millisecond), + }, nil + } + // Subsequent calls: return authentication error + retrieveErr := createRetrieveError(http.StatusUnauthorized, `{"error":"invalid_token"}`) + return nil, retrieveErr + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Expect SetWorkloadStatus to be called when auth error occurs + statusManager.EXPECT(). + SetWorkloadStatus( + gomock.Any(), + "test-workload", + rt.WorkloadStatusUnauthenticated, + gomock.Any(), + ). + Return(nil). + Times(1) + + ats.StartBackgroundMonitoring() + + // Wait for token to expire and background monitoring to detect failure + // The timer is scheduled for when token expires (500ms), then it processes the error + // Need enough time for: initial timer (1ms) + token expiry (500ms) + error processing + time.Sleep(2 * time.Second) + + // Verify monitoring stopped by checking that SetWorkloadStatus was called + // (the mock expectations already verify this) +} + +func TestMonitoredTokenSource_BackgroundMonitoringStopsOnAnyError(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, statusManager := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + callCount := 0 + // Use a generic error - should mark as unauthenticated and stop monitoring + genericErr := errors.New("network timeout") + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + callCount++ + if callCount == 1 { + // First call: return valid token with short expiry + return &oauth2.Token{ + AccessToken: "test-token", + RefreshToken: "test-refresh", + Expiry: time.Now().Add(500 * time.Millisecond), + }, nil + } + // Subsequent calls: return generic error (should mark as unauthenticated) + return nil, genericErr + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Expect SetWorkloadStatus to be called when any error occurs + statusManager.EXPECT(). + SetWorkloadStatus( + gomock.Any(), + "test-workload", + rt.WorkloadStatusUnauthenticated, + gomock.Any(), + ). + Return(nil). + Times(1) + + ats.StartBackgroundMonitoring() + + // Wait for token to expire and background monitoring to detect failure + // Flow: initial timer (1ms) → first check (gets token) → reschedule → wait → second check (gets error) → mark unauthenticated + time.Sleep(2 * time.Second) + + // Verify monitoring stopped by checking that SetWorkloadStatus was called + // (the mock expectations already verify this) +} + +func TestMonitoredTokenSource_ExpiredTokenHandling(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, _ := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + // Return an already-expired token (oauth2 library should try to refresh) + expiredToken := &oauth2.Token{ + AccessToken: "expired-token", + RefreshToken: "refresh-token", + Expiry: time.Now().Add(-time.Hour), + } + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return expiredToken, nil + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + // Should not mark as unauthenticated just for expired token + // (oauth2 library should handle refresh; we only mark on actual auth errors) + // (no expectations set means we expect SetWorkloadStatus not to be called) + + ats.StartBackgroundMonitoring() + + // Wait a bit for monitoring to check + time.Sleep(200 * time.Millisecond) + + cancel() +} + +func TestMonitoredTokenSource_StopMonitoring(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, _ := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: "test-token", + RefreshToken: "refresh", + Expiry: time.Now().Add(time.Hour), + }, nil + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + ats.StartBackgroundMonitoring() + + // Wait a bit to ensure monitoring started + time.Sleep(100 * time.Millisecond) + + // Stop monitoring via context cancellation + cancel() + + // Wait a bit for monitoring to stop + time.Sleep(100 * time.Millisecond) + + // Verify monitoring stopped - context cancellation is handled internally + // We can verify by ensuring no unexpected SetWorkloadStatus calls + // (test passes if no errors occur) +} + +func TestMonitoredTokenSource_MultipleCallsToToken(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + statusUpdater, statusManager := newMockStatusUpdater(ctrl) + tokenSource := newMockTokenSource() + + retrieveErr := createRetrieveError(http.StatusUnauthorized, `{"error":"invalid_token"}`) + tokenSource.setTokenFn(func() (*oauth2.Token, error) { + return nil, retrieveErr + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ats := NewMonitoredTokenSource(ctx, tokenSource, "test-workload", statusUpdater) + + statusManager.EXPECT(). + SetWorkloadStatus( + gomock.Any(), + "test-workload", + rt.WorkloadStatusUnauthenticated, + gomock.Any(), + ). + Return(nil). + Times(3) // Each Token() call will mark it + + // Call Token() multiple times + for i := 0; i < 3; i++ { + _, err := ats.Token() + if err == nil { + t.Fatal("Expected error, got nil") + } + } + + time.Sleep(50 * time.Millisecond) +} diff --git a/pkg/container/runtime/types.go b/pkg/container/runtime/types.go index a8206a84c..2bbdc75e0 100644 --- a/pkg/container/runtime/types.go +++ b/pkg/container/runtime/types.go @@ -36,6 +36,9 @@ const ( WorkloadStatusRemoving WorkloadStatus = "removing" // WorkloadStatusUnknown indicates that the workload status is unknown. WorkloadStatusUnknown WorkloadStatus = "unknown" + // WorkloadStatusUnauthenticated indicates that the workload is running but + // cannot authenticate with the remote MCP server (e.g., expired refresh token). + WorkloadStatusUnauthenticated WorkloadStatus = "unauthenticated" ) // ContainerInfo represents information about a container diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index c8f7ff34d..48b37f8b7 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -12,6 +12,7 @@ import ( "golang.org/x/oauth2" + "github.com/stacklok/toolhive/pkg/auth" "github.com/stacklok/toolhive/pkg/auth/remote" "github.com/stacklok/toolhive/pkg/client" "github.com/stacklok/toolhive/pkg/config" @@ -51,6 +52,29 @@ type Runner struct { prometheusHandler http.Handler statusManager statuses.StatusManager + + // authenticatedTokenSource is the wrapped token source for remote workloads with authentication monitoring + authenticatedTokenSource *auth.MonitoredTokenSource + + // monitoringCtx is the context for background authentication monitoring + // It is cancelled during Cleanup() to stop monitoring + monitoringCtx context.Context + monitoringCancel context.CancelFunc +} + +// statusManagerAdapter adapts statuses.StatusManager to auth.StatusUpdater interface +type statusManagerAdapter struct { + sm statuses.StatusManager +} + +func (a *statusManagerAdapter) SetWorkloadStatus( + ctx context.Context, + workloadName string, + status rt.WorkloadStatus, + reason string, +) error { + logger.Debugf("Setting workload status: %s, %s, %s", workloadName, status, reason) + return a.sm.SetWorkloadStatus(ctx, workloadName, status, reason) } // NewRunner creates a new Runner with the provided configuration @@ -225,6 +249,17 @@ func (r *Runner) Run(ctx context.Context) error { return fmt.Errorf("failed to authenticate to remote server: %w", err) } + // Wrap the token source with authentication monitoring for remote workloads + if tokenSource != nil { + // Create a child context for monitoring that can be cancelled during cleanup + r.monitoringCtx, r.monitoringCancel = context.WithCancel(ctx) + // Create adapter to bridge statuses.StatusManager to auth.StatusUpdater + adapter := &statusManagerAdapter{sm: r.statusManager} + r.authenticatedTokenSource = auth.NewMonitoredTokenSource(r.monitoringCtx, tokenSource, r.Config.BaseName, adapter) + tokenSource = r.authenticatedTokenSource + r.authenticatedTokenSource.StartBackgroundMonitoring() + } + // Set the token source on the HTTP transport if httpTransport, ok := transportHandler.(interface{ SetTokenSource(oauth2.TokenSource) }); ok { httpTransport.SetTokenSource(tokenSource) @@ -405,5 +440,13 @@ func (r *Runner) Cleanup(ctx context.Context) error { lastErr = err } } + + // Stop background authentication monitoring for remote workloads + // Cancel the monitoring context to stop the background goroutine + if r.monitoringCancel != nil { + r.monitoringCancel() + r.monitoringCancel = nil + } + return lastErr } diff --git a/pkg/transport/http.go b/pkg/transport/http.go index 3ce3d2351..90e0431ee 100644 --- a/pkg/transport/http.go +++ b/pkg/transport/http.go @@ -13,6 +13,7 @@ import ( rt "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/transport/errors" + "github.com/stacklok/toolhive/pkg/transport/middleware" "github.com/stacklok/toolhive/pkg/transport/proxy/transparent" "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -108,23 +109,7 @@ func (t *HTTPTransport) SetTokenSource(tokenSource oauth2.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) - }) - } + return middleware.CreateTokenInjectionMiddleware(t.tokenSource) } // Mode returns the transport mode. diff --git a/pkg/transport/middleware/token_injection.go b/pkg/transport/middleware/token_injection.go new file mode 100644 index 000000000..c2e8ac683 --- /dev/null +++ b/pkg/transport/middleware/token_injection.go @@ -0,0 +1,34 @@ +// Package middleware provides middleware functions for the transport package. +package middleware + +import ( + "fmt" + "net/http" + + "golang.org/x/oauth2" + + "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/transport/types" +) + +// CreateTokenInjectionMiddleware returns a middleware that injects a Bearer token +// from the provided oauth2.TokenSource. It returns 401 when the workload is unauthenticated. +func CreateTokenInjectionMiddleware(tokenSource oauth2.TokenSource) types.MiddlewareFunction { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tokenSource != nil { + token, err := tokenSource.Token() + if err != nil { + logger.Warnf("Unable to retrieve OAuth token: %v", err) + // The token source (AuthenticatedTokenSource) handles marking + // the workload as unauthenticated in its Token() method + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } + + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/pkg/vmcp/aggregator/cli_discoverer.go b/pkg/vmcp/aggregator/cli_discoverer.go index f2c5d5c9b..c577a93be 100644 --- a/pkg/vmcp/aggregator/cli_discoverer.go +++ b/pkg/vmcp/aggregator/cli_discoverer.go @@ -145,6 +145,8 @@ func mapWorkloadStatusToHealth(status rt.WorkloadStatus) vmcp.BackendHealthStatu return vmcp.BackendUnhealthy case rt.WorkloadStatusStarting, rt.WorkloadStatusUnknown: return vmcp.BackendUnknown + case rt.WorkloadStatusUnauthenticated: + return vmcp.BackendUnauthenticated default: return vmcp.BackendUnknown } diff --git a/pkg/vmcp/types.go b/pkg/vmcp/types.go index 70d7f3b7b..4a29098e4 100644 --- a/pkg/vmcp/types.go +++ b/pkg/vmcp/types.go @@ -81,6 +81,9 @@ const ( // BackendUnknown indicates the backend health status is unknown. BackendUnknown BackendHealthStatus = "unknown" + + // BackendUnauthenticated indicates the backend is not authenticated. + BackendUnauthenticated BackendHealthStatus = "unauthenticated" ) // Backend represents a discovered backend MCP server workload.