From 7755fa1aeed9e0ea70943cddbf72ea50e79d802d Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 30 Oct 2025 17:02:27 +0200 Subject: [PATCH 01/20] Initial PoC of supporting the official MCP registry Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/config.go | 44 +++++ docs/arch/06-registry-system.md | 117 ++++++++++- go.mod | 8 +- go.sum | 28 +-- pkg/api/v1/registry.go | 7 + pkg/config/config.go | 2 + pkg/config/interface.go | 16 ++ pkg/config/mocks/mock_provider.go | 14 ++ pkg/config/registry.go | 58 ++++++ pkg/registry/api/client.go | 302 ++++++++++++++++++++++++++++ pkg/registry/factory.go | 15 ++ pkg/registry/provider_api.go | 315 ++++++++++++++++++++++++++++++ 12 files changed, 907 insertions(+), 19 deletions(-) create mode 100644 pkg/registry/api/client.go create mode 100644 pkg/registry/provider_api.go diff --git a/cmd/thv/app/config.go b/cmd/thv/app/config.go index 8b51df825..0ff1def1d 100644 --- a/cmd/thv/app/config.go +++ b/cmd/thv/app/config.go @@ -56,6 +56,22 @@ Examples: RunE: setRegistryCmdFunc, } +var setRegistryAPICmd = &cobra.Command{ + Use: "set-registry-api ", + Short: "Set the MCP Registry API endpoint", + Long: `Set the MCP Registry API endpoint that implements the MCP Registry API v0.1 specification. +This enables on-demand querying of servers from a live registry API. + +The API endpoint must implement the official MCP Registry API specification from +https://registry.modelcontextprotocol.io/docs + +Examples: + thv config set-registry-api https://registry.example.com # API endpoint + thv config set-registry-api https://api.example.com --allow-private-ip # With private IP support`, + Args: cobra.ExactArgs(1), + RunE: setRegistryAPICmdFunc, +} + var getRegistryCmd = &cobra.Command{ Use: "get-registry", Short: "Get the currently configured registry", @@ -97,6 +113,14 @@ func init() { false, "Allow setting the registry URL, even if it references a private IP address", ) + configCmd.AddCommand(setRegistryAPICmd) + setRegistryAPICmd.Flags().BoolVarP( + &allowPrivateRegistryIp, + "allow-private-ip", + "p", + false, + "Allow setting the registry API URL, even if it references a private IP address", + ) configCmd.AddCommand(getRegistryCmd) configCmd.AddCommand(unsetRegistryCmd) configCmd.AddCommand(usageMetricsCmd) @@ -183,11 +207,31 @@ func setRegistryCmdFunc(_ *cobra.Command, args []string) error { } } +func setRegistryAPICmdFunc(_ *cobra.Command, args []string) error { + apiURL := args[0] + provider := config.NewDefaultProvider() + + err := provider.SetRegistryAPI(apiURL, allowPrivateRegistryIp) + if err != nil { + return err + } + + fmt.Printf("Successfully set registry API endpoint: %s\n", apiURL) + if allowPrivateRegistryIp { + fmt.Print("Successfully enabled use of private IP addresses for the registry API\n") + fmt.Print("Caution: allowing registry API URLs containing private IP addresses may decrease your security.\n" + + "Make sure you trust any registry APIs you configure with ToolHive.\n") + } + return nil +} + func getRegistryCmdFunc(_ *cobra.Command, _ []string) error { provider := config.NewDefaultProvider() url, localPath, _, registryType := provider.GetRegistryConfig() switch registryType { + case config.RegistryTypeAPI: + fmt.Printf("Current registry: %s (API endpoint)\n", url) case config.RegistryTypeURL: fmt.Printf("Current registry: %s (remote URL)\n", url) case config.RegistryTypeFile: diff --git a/docs/arch/06-registry-system.md b/docs/arch/06-registry-system.md index 04ac61223..311731f37 100644 --- a/docs/arch/06-registry-system.md +++ b/docs/arch/06-registry-system.md @@ -321,19 +321,124 @@ Remote registries can be configured in the ToolHive configuration file to fetch **Implementation**: `pkg/registry/provider.go`, `pkg/registry/provider_local.go`, `pkg/registry/provider_remote.go`, `pkg/registry/factory.go` +### API Registry Provider + +ToolHive supports live MCP Registry API endpoints that implement the official [MCP Registry API v0.1 specification](https://registry.modelcontextprotocol.io/docs). This enables on-demand querying of servers from dynamic registry APIs. + +**Key differences from Remote Registry:** +- **On-demand queries**: Fetches servers as needed, not bulk download +- **Live data**: Always queries the latest data from the API +- **Standard protocol**: Uses official MCP Registry API specification +- **Pagination support**: Handles large registries via cursor-based pagination +- **Search capabilities**: Supports server search via API queries + +**Set API registry:** +```bash +thv config set-registry-api https://registry.example.com +``` + +**With private IP support:** +```bash +thv config set-registry-api https://registry.internal.company.com --allow-private-ip +``` + +**Check current registry:** +```bash +thv config get-registry +# Output: Current registry: https://registry.example.com (API endpoint) +``` + +**Unset API registry:** +```bash +thv config unset-registry +``` + +**API Requirements:** + +The API endpoint must implement: +- `GET /v0/servers` - List all servers with pagination +- `GET /v0/servers/:name` - Get specific server by reverse-DNS name +- `GET /v0/servers?search=` - Search servers +- `GET /openapi.yaml` - OpenAPI specification (version 1.0.0) + +**Response format:** + +Servers are returned in the upstream [MCP Registry format](https://github.com/modelcontextprotocol/registry): + +```json +{ + "server": { + "name": "io.github.example/weather", + "description": "Weather information MCP server", + "packages": [ + { + "registry_type": "oci", + "identifier": "ghcr.io/example/weather-mcp:v1.0.0", + "version": "v1.0.0" + } + ], + "remotes": [], + "repository": { + "type": "git", + "url": "https://github.com/example/weather-mcp" + } + } +} +``` + +**Type conversion:** + +ToolHive automatically converts upstream MCP Registry types to internal format: + +- **Container servers**: `packages` with `registry_type: "oci"` → `ImageMetadata` +- **Remote servers**: `remotes` with SSE/HTTP transport → `RemoteServerMetadata` +- **Package formats**: + - `oci`/`docker` → Docker image reference + - `npm` → `npx://@` + - `pypi` → `uvx://@` + +**Implementation**: +- `pkg/registry/api/client.go` - MCP Registry API client +- `pkg/registry/provider_api.go` - API provider implementation with type conversion +- `pkg/config/registry.go` - Configuration methods (`setRegistryAPI`) +- `pkg/registry/factory.go` - Provider factory with API support +- `cmd/thv/app/config.go` - CLI commands + +**Use cases:** +- Connect to official MCP Registry at https://registry.modelcontextprotocol.io +- Point to organization's private MCP Registry API +- Use third-party registry services +- Dynamic server catalogs that update frequently + ### Registry Priority -When multiple registries configured: +When multiple registries configured, ToolHive uses this priority order: + +1. **API Registry** (if configured) - Highest priority for live data +2. **Remote Registry** (if configured) - Static remote registry URL +3. **Local Registry** (if configured) - Custom local file +4. **Built-in Registry** - Default embedded registry -1. **Custom registries** (in order added) -2. **Built-in registry** +The factory selects the first configured registry type in this order. To switch between registry types, use the appropriate CLI command: -First match wins. Use registry namespacing to avoid conflicts: ```bash -thv run custom-registry/server-name +# Set API registry (highest priority) +thv config set-registry-api https://registry.example.com + +# Set remote registry (if no API registry configured) +thv config set-registry https://example.com/registry.json + +# Set local registry (if no API/remote registry configured) +thv config set-registry /path/to/registry.json + +# Check current registry configuration +thv config get-registry + +# Remove custom registry (fall back to built-in) +thv config unset-registry ``` -**Implementation**: `pkg/registry/provider.go`, `pkg/registry/provider_local.go`, `pkg/registry/provider_remote.go`, `pkg/registry/factory.go` +**Implementation**: `pkg/registry/factory.go`, `pkg/registry/provider.go`, `pkg/registry/provider_local.go`, `pkg/registry/provider_remote.go`, `pkg/registry/provider_api.go` ## Registry API Server diff --git a/go.mod b/go.mod index bb298ae45..f1db59c55 100644 --- a/go.mod +++ b/go.mod @@ -67,11 +67,14 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.8.2 // indirect cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/kms v1.23.0 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/spanner v1.84.1 // indirect cloud.google.com/go/storage v1.56.2 // indirect dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect @@ -99,6 +102,7 @@ require ( github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/coreos/go-oidc/v3 v3.16.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cristalhq/jwt/v4 v4.0.2 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect @@ -125,7 +129,7 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect @@ -196,6 +200,7 @@ require ( github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/term v0.5.2 // indirect + github.com/modelcontextprotocol/registry v1.3.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -238,6 +243,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/stacklok/toolhive-registry v0.0.0-20251030120119-6acb3dd385b5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sv-tools/openapi v0.2.1 // indirect diff --git a/go.sum b/go.sum index fd09ddb9d..23e90d02d 100644 --- a/go.sum +++ b/go.sum @@ -342,8 +342,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= -cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= +cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= @@ -631,10 +631,10 @@ github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXh github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= @@ -643,8 +643,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfg github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= @@ -793,8 +793,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= +github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -928,8 +928,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= @@ -1403,6 +1403,8 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modelcontextprotocol/registry v1.3.7 h1:FR8vcBPBE0nRAz0P5lNGkiZmdtokZIyTry3Lx7J0nhE= +github.com/modelcontextprotocol/registry v1.3.7/go.mod h1:v2pC//yAoG3PPlkvXPrBI+CQd/5sar+MYCxWioL/NDk= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1590,6 +1592,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stacklok/toolhive-registry v0.0.0-20251030120119-6acb3dd385b5 h1:pu+6Tx4hC/mlpuR2czMr63Yg8e8s92L1NSL6Epxp2d4= +github.com/stacklok/toolhive-registry v0.0.0-20251030120119-6acb3dd385b5/go.mod h1:B7b+U7y0NA+iTBo4mDUmwTKhPK6s92gNcIxjMM+nXQQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= diff --git a/pkg/api/v1/registry.go b/pkg/api/v1/registry.go index 85a1f2dee..9ab8f5695 100644 --- a/pkg/api/v1/registry.go +++ b/pkg/api/v1/registry.go @@ -25,6 +25,8 @@ const ( RegistryTypeFile RegistryType = "file" // RegistryTypeURL represents a remote URL registry RegistryTypeURL RegistryType = "url" + // RegistryTypeAPI represents an MCP Registry API endpoint + RegistryTypeAPI RegistryType = "api" // RegistryTypeDefault represents a built-in registry RegistryTypeDefault RegistryType = "default" ) @@ -38,6 +40,11 @@ func (rr *RegistryRoutes) getRegistryInfo() (RegistryType, string) { func getRegistryInfoWithProvider(configProvider config.Provider) (RegistryType, string) { cfg := configProvider.GetConfig() + // Check API URL first (highest priority for live data) + if cfg.RegistryApiUrl != "" { + return RegistryTypeAPI, cfg.RegistryApiUrl + } + if cfg.RegistryUrl != "" { return RegistryTypeURL, cfg.RegistryUrl } diff --git a/pkg/config/config.go b/pkg/config/config.go index bbb80b635..4eebcd23f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,6 +27,7 @@ type Config struct { Secrets Secrets `yaml:"secrets"` Clients Clients `yaml:"clients"` RegistryUrl string `yaml:"registry_url"` + RegistryApiUrl string `yaml:"registry_api_url"` LocalRegistryPath string `yaml:"local_registry_path"` AllowPrivateRegistryIp bool `yaml:"allow_private_registry_ip"` CACertificatePath string `yaml:"ca_certificate_path,omitempty"` @@ -102,6 +103,7 @@ func createNewConfigWithDefaults() Config { SetupCompleted: false, }, RegistryUrl: "", + RegistryApiUrl: "", AllowPrivateRegistryIp: false, DefaultGroupMigration: false, } diff --git a/pkg/config/interface.go b/pkg/config/interface.go index 97467c333..39afd332a 100644 --- a/pkg/config/interface.go +++ b/pkg/config/interface.go @@ -14,6 +14,7 @@ type Provider interface { // Registry operations SetRegistryURL(registryURL string, allowPrivateRegistryIp bool) error + SetRegistryAPI(apiURL string, allowPrivateRegistryIp bool) error SetRegistryFile(registryPath string) error UnsetRegistry() error GetRegistryConfig() (url, localPath string, allowPrivateIP bool, registryType string) @@ -52,6 +53,11 @@ func (d *DefaultProvider) SetRegistryURL(registryURL string, allowPrivateRegistr return setRegistryURL(d, registryURL, allowPrivateRegistryIp) } +// SetRegistryAPI validates and sets an MCP Registry API endpoint +func (d *DefaultProvider) SetRegistryAPI(apiURL string, allowPrivateRegistryIp bool) error { + return setRegistryAPI(d, apiURL, allowPrivateRegistryIp) +} + // SetRegistryFile validates and sets a local registry file func (d *DefaultProvider) SetRegistryFile(registryPath string) error { return setRegistryFile(d, registryPath) @@ -118,6 +124,11 @@ func (p *PathProvider) SetRegistryURL(registryURL string, allowPrivateRegistryIp return setRegistryURL(p, registryURL, allowPrivateRegistryIp) } +// SetRegistryAPI validates and sets an MCP Registry API endpoint +func (p *PathProvider) SetRegistryAPI(apiURL string, allowPrivateRegistryIp bool) error { + return setRegistryAPI(p, apiURL, allowPrivateRegistryIp) +} + // SetRegistryFile validates and sets a local registry file func (p *PathProvider) SetRegistryFile(registryPath string) error { return setRegistryFile(p, registryPath) @@ -179,6 +190,11 @@ func (*KubernetesProvider) SetRegistryURL(_ string, _ bool) error { return nil } +// SetRegistryAPI is a no-op for Kubernetes environments +func (*KubernetesProvider) SetRegistryAPI(_ string, _ bool) error { + return nil +} + // SetRegistryFile is a no-op for Kubernetes environments func (*KubernetesProvider) SetRegistryFile(_ string) error { return nil diff --git a/pkg/config/mocks/mock_provider.go b/pkg/config/mocks/mock_provider.go index ceef8d14c..040507b29 100644 --- a/pkg/config/mocks/mock_provider.go +++ b/pkg/config/mocks/mock_provider.go @@ -116,6 +116,20 @@ func (mr *MockProviderMockRecorder) SetCACert(certPath any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCACert", reflect.TypeOf((*MockProvider)(nil).SetCACert), certPath) } +// SetRegistryAPI mocks base method. +func (m *MockProvider) SetRegistryAPI(apiURL string, allowPrivateRegistryIp bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRegistryAPI", apiURL, allowPrivateRegistryIp) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRegistryAPI indicates an expected call of SetRegistryAPI. +func (mr *MockProviderMockRecorder) SetRegistryAPI(apiURL, allowPrivateRegistryIp any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRegistryAPI", reflect.TypeOf((*MockProvider)(nil).SetRegistryAPI), apiURL, allowPrivateRegistryIp) +} + // SetRegistryFile mocks base method. func (m *MockProvider) SetRegistryFile(registryPath string) error { m.ctrl.T.Helper() diff --git a/pkg/config/registry.go b/pkg/config/registry.go index aed49c89c..175846e88 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -13,6 +13,8 @@ const ( RegistryTypeFile = "file" // RegistryTypeURL represents a remote URL registry RegistryTypeURL = "url" + // RegistryTypeAPI represents an MCP Registry API endpoint + RegistryTypeAPI = "api" ) // DetectRegistryType determines if input is a URL or file path and returns cleaned path @@ -95,10 +97,61 @@ func setRegistryFile(provider Provider, registryPath string) error { return nil } +// setRegistryAPI validates and sets an MCP Registry API URL using the provided provider +func setRegistryAPI(provider Provider, apiURL string, allowPrivateRegistryIp bool) error { + parsedURL, err := neturl.Parse(apiURL) + if err != nil { + return fmt.Errorf("invalid registry API URL: %w", err) + } + + if allowPrivateRegistryIp { + // we validate either https or http URLs + if parsedURL.Scheme != networking.HttpScheme && parsedURL.Scheme != networking.HttpsScheme { + return fmt.Errorf("registry API URL must start with http:// or https:// when allowing private IPs") + } + } else { + // we just allow https + if parsedURL.Scheme != networking.HttpsScheme { + return fmt.Errorf("registry API URL must start with https:// when not allowing private IPs") + } + } + + if !allowPrivateRegistryIp { + registryClient, err := networking.NewHttpClientBuilder().Build() + if err != nil { + return fmt.Errorf("failed to create HTTP client: %w", err) + } + // Try to fetch the /openapi.yaml endpoint to validate + openapiURL := apiURL + if !strings.HasSuffix(apiURL, "/") { + openapiURL += "/" + } + openapiURL += "openapi.yaml" + _, err = registryClient.Get(openapiURL) + if err != nil && strings.Contains(fmt.Sprint(err), networking.ErrPrivateIpAddress) { + return err + } + } + + // Update the configuration + err = provider.UpdateConfig(func(c *Config) { + c.RegistryApiUrl = apiURL + c.RegistryUrl = "" // Clear static registry URL when setting API URL + c.LocalRegistryPath = "" // Clear local path when setting API URL + c.AllowPrivateRegistryIp = allowPrivateRegistryIp + }) + if err != nil { + return fmt.Errorf("failed to update configuration: %w", err) + } + + return nil +} + // unsetRegistry resets registry configuration to defaults using the provided provider func unsetRegistry(provider Provider) error { err := provider.UpdateConfig(func(c *Config) { c.RegistryUrl = "" + c.RegistryApiUrl = "" c.LocalRegistryPath = "" c.AllowPrivateRegistryIp = false }) @@ -112,6 +165,11 @@ func unsetRegistry(provider Provider) error { func getRegistryConfig(provider Provider) (url, localPath string, allowPrivateIP bool, registryType string) { cfg := provider.GetConfig() + // Check API URL first (highest priority for live data) + if cfg.RegistryApiUrl != "" { + return cfg.RegistryApiUrl, "", cfg.AllowPrivateRegistryIp, RegistryTypeAPI + } + if cfg.RegistryUrl != "" { return cfg.RegistryUrl, "", cfg.AllowPrivateRegistryIp, RegistryTypeURL } diff --git a/pkg/registry/api/client.go b/pkg/registry/api/client.go new file mode 100644 index 000000000..17eb2662b --- /dev/null +++ b/pkg/registry/api/client.go @@ -0,0 +1,302 @@ +// Package api provides client functionality for interacting with MCP Registry API endpoints +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "gopkg.in/yaml.v3" + + "github.com/stacklok/toolhive/pkg/networking" +) + +// Client represents an MCP Registry API client +type Client interface { + // GetServer retrieves a single server by its reverse-DNS name + GetServer(ctx context.Context, name string) (*v0.ServerJSON, error) + + // ListServers retrieves all servers with automatic pagination handling + ListServers(ctx context.Context, opts *ListOptions) ([]*v0.ServerJSON, error) + + // SearchServers searches for servers matching the query string + SearchServers(ctx context.Context, query string) ([]*v0.ServerJSON, error) + + // ValidateEndpoint validates that the endpoint implements the MCP Registry API + ValidateEndpoint(ctx context.Context) error +} + +// ListOptions contains options for listing servers +type ListOptions struct { + // Limit is the maximum number of servers to retrieve per page (default: 100) + Limit int + + // UpdatedSince filters servers updated after this RFC3339 timestamp + UpdatedSince string + + // Version filters servers by version (e.g., "latest") + Version string +} + +// mcpRegistryClient implements the Client interface for MCP Registry v0.1 API +type mcpRegistryClient struct { + baseURL string + httpClient *http.Client + allowPrivateIp bool +} + +// NewClient creates a new MCP Registry API client +func NewClient(baseURL string, allowPrivateIp bool) (Client, error) { + // Build HTTP client with security controls + httpClient, err := networking.NewHttpClientBuilder(). + WithPrivateIPs(allowPrivateIp). + Build() + if err != nil { + return nil, fmt.Errorf("failed to build HTTP client: %w", err) + } + + // Ensure base URL doesn't have trailing slash + if baseURL[len(baseURL)-1] == '/' { + baseURL = baseURL[:len(baseURL)-1] + } + + return &mcpRegistryClient{ + baseURL: baseURL, + httpClient: httpClient, + allowPrivateIp: allowPrivateIp, + }, nil +} + +// GetServer retrieves a single server by its reverse-DNS name +func (c *mcpRegistryClient) GetServer(ctx context.Context, name string) (*v0.ServerJSON, error) { + // URL encode the server name to handle special characters + encodedName := url.PathEscape(name) + endpoint := fmt.Sprintf("%s/v0/servers/%s", c.baseURL, encodedName) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch server %s: %w", name, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) + } + + var serverResp v0.ServerResponse + if err := json.NewDecoder(resp.Body).Decode(&serverResp); err != nil { + return nil, fmt.Errorf("failed to decode server response: %w", err) + } + + return &serverResp.Server, nil +} + +// ListServers retrieves all servers with automatic pagination handling +func (c *mcpRegistryClient) ListServers(ctx context.Context, opts *ListOptions) ([]*v0.ServerJSON, error) { + if opts == nil { + opts = &ListOptions{Limit: 100} + } + if opts.Limit == 0 { + opts.Limit = 100 + } + + var allServers []*v0.ServerJSON + cursor := "" + + // Pagination loop - continue until no more cursors + for { + servers, nextCursor, err := c.fetchServersPage(ctx, cursor, opts) + if err != nil { + return nil, err + } + + allServers = append(allServers, servers...) + + // Check if we have more pages + if nextCursor == "" { + break + } + + cursor = nextCursor + + // Safety limit: prevent infinite loops + if len(allServers) > 10000 { + return nil, fmt.Errorf("exceeded maximum server limit (10000)") + } + } + + return allServers, nil +} + +// fetchServersPage fetches a single page of servers +func (c *mcpRegistryClient) fetchServersPage(ctx context.Context, cursor string, opts *ListOptions) ([]*v0.ServerJSON, string, error) { + endpoint := fmt.Sprintf("%s/v0/servers", c.baseURL) + + // Build query parameters + params := url.Values{} + if cursor != "" { + params.Add("cursor", cursor) + } + if opts.Limit > 0 { + params.Add("limit", fmt.Sprintf("%d", opts.Limit)) + } + if opts.UpdatedSince != "" { + params.Add("updated_since", opts.UpdatedSince) + } + if opts.Version != "" { + params.Add("version", opts.Version) + } + + if len(params) > 0 { + endpoint = fmt.Sprintf("%s?%s", endpoint, params.Encode()) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, "", fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, "", fmt.Errorf("failed to fetch servers: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) + } + + var listResp v0.ServerListResponse + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { + return nil, "", fmt.Errorf("failed to decode list response: %w", err) + } + + // Extract ServerJSON from ServerResponse entries + servers := make([]*v0.ServerJSON, len(listResp.Servers)) + for i, serverResp := range listResp.Servers { + servers[i] = &serverResp.Server + } + + return servers, listResp.Metadata.NextCursor, nil +} + +// SearchServers searches for servers matching the query string +func (c *mcpRegistryClient) SearchServers(ctx context.Context, query string) ([]*v0.ServerJSON, error) { + endpoint := fmt.Sprintf("%s/v0/servers?search=%s", c.baseURL, url.QueryEscape(query)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to search servers: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) + } + + var listResp v0.ServerListResponse + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { + return nil, fmt.Errorf("failed to decode search response: %w", err) + } + + // Extract ServerJSON from ServerResponse entries + servers := make([]*v0.ServerJSON, len(listResp.Servers)) + for i, serverResp := range listResp.Servers { + servers[i] = &serverResp.Server + } + + return servers, nil +} + +// ValidateEndpoint validates that the endpoint implements the MCP Registry API +// by checking for the presence of /openapi.yaml with correct version and description +func (c *mcpRegistryClient) ValidateEndpoint(ctx context.Context) error { + endpoint := fmt.Sprintf("%s/openapi.yaml", c.baseURL) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to fetch /openapi.yaml: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("/openapi.yaml not found (status %d) - not a valid MCP Registry API", resp.StatusCode) + } + + // Parse the OpenAPI spec + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read /openapi.yaml: %w", err) + } + + var openapiSpec map[string]interface{} + if err := yaml.Unmarshal(data, &openapiSpec); err != nil { + return fmt.Errorf("failed to parse /openapi.yaml: %w", err) + } + + // Check for 'info' section + info, ok := openapiSpec["info"].(map[string]interface{}) + if !ok { + return fmt.Errorf("/openapi.yaml missing 'info' section") + } + + // Check version + version, ok := info["version"].(string) + if !ok { + return fmt.Errorf("/openapi.yaml info section missing 'version' field") + } + + // MCP Registry API should be version 1.0.0 + if version != "1.0.0" { + return fmt.Errorf("/openapi.yaml version is %s, expected 1.0.0", version) + } + + // Check description contains GitHub URL + description, ok := info["description"].(string) + if !ok { + return fmt.Errorf("/openapi.yaml info section missing 'description' field") + } + + expectedGitHubURL := "https://github.com/modelcontextprotocol/registry" + if !contains(description, expectedGitHubURL) { + return fmt.Errorf("/openapi.yaml description does not contain expected GitHub URL: %s", expectedGitHubURL) + } + + return nil +} + +// contains checks if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsRec(s, substr)) +} + +func containsRec(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/registry/factory.go b/pkg/registry/factory.go index 89b06c29d..f1f77a48c 100644 --- a/pkg/registry/factory.go +++ b/pkg/registry/factory.go @@ -19,6 +19,21 @@ var ( // NewRegistryProvider creates a new registry provider based on the configuration func NewRegistryProvider(cfg *config.Config) Provider { + // Priority order: + // 1. API URL (if configured) - for live MCP Registry API queries + // 2. Remote URL (if configured) - for static JSON over HTTP + // 3. Local file path (if configured) - for local JSON file + // 4. Default - embedded registry data + + if cfg != nil && len(cfg.RegistryApiUrl) > 0 { + provider, err := NewAPIRegistryProvider(cfg.RegistryApiUrl, cfg.AllowPrivateRegistryIp) + if err != nil { + // Log error but fall back to default provider + // This prevents application from failing if API is temporarily unavailable + return NewLocalRegistryProvider() + } + return provider + } if cfg != nil && len(cfg.RegistryUrl) > 0 { return NewRemoteRegistryProvider(cfg.RegistryUrl, cfg.AllowPrivateRegistryIp) } diff --git a/pkg/registry/provider_api.go b/pkg/registry/provider_api.go new file mode 100644 index 000000000..348ed1546 --- /dev/null +++ b/pkg/registry/provider_api.go @@ -0,0 +1,315 @@ +package registry + +import ( + "context" + "fmt" + "strings" + "time" + + v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + + "github.com/stacklok/toolhive/pkg/registry/api" +) + +// APIRegistryProvider provides registry data from an MCP Registry API endpoint +// It queries the API on-demand for each operation, ensuring fresh data. +type APIRegistryProvider struct { + *BaseProvider + apiURL string + allowPrivateIp bool + client api.Client +} + +// NewAPIRegistryProvider creates a new API registry provider +func NewAPIRegistryProvider(apiURL string, allowPrivateIp bool) (*APIRegistryProvider, error) { + // Create API client + client, err := api.NewClient(apiURL, allowPrivateIp) + if err != nil { + return nil, fmt.Errorf("failed to create API client: %w", err) + } + + p := &APIRegistryProvider{ + apiURL: apiURL, + allowPrivateIp: allowPrivateIp, + client: client, + } + + // Initialize the base provider with the GetRegistry function + p.BaseProvider = NewBaseProvider(p.GetRegistry) + + // Validate the endpoint + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := client.ValidateEndpoint(ctx); err != nil { + return nil, fmt.Errorf("invalid MCP Registry API endpoint: %w", err) + } + + return p, nil +} + +// GetRegistry returns the registry data by fetching all servers from the API +// This method queries the API and converts all servers to ToolHive format. +// Note: This can be slow for large registries as it fetches everything. +func (p *APIRegistryProvider) GetRegistry() (*Registry, error) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Fetch all servers from the API + servers, err := p.client.ListServers(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to list servers from API: %w", err) + } + + // Convert servers to ToolHive format + serverMetadata, err := ConvertServersToMetadata(servers) + if err != nil { + return nil, fmt.Errorf("failed to convert servers to ToolHive format: %w", err) + } + + // Build Registry structure + registry := &Registry{ + Version: "1.0.0", + LastUpdated: time.Now().Format(time.RFC3339), + Servers: make(map[string]*ImageMetadata), + RemoteServers: make(map[string]*RemoteServerMetadata), + Groups: []*Group{}, + } + + // Separate servers into container and remote + for _, server := range serverMetadata { + if server.IsRemote() { + if remoteServer, ok := server.(*RemoteServerMetadata); ok { + registry.RemoteServers[remoteServer.Name] = remoteServer + } + } else { + if imageServer, ok := server.(*ImageMetadata); ok { + registry.Servers[imageServer.Name] = imageServer + } + } + } + + return registry, nil +} + +// GetServer returns a specific server by name (queries API directly) +func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Try to find server by searching (since API uses reverse-DNS names) + // First try direct lookup by assuming simple name + servers, err := p.client.SearchServers(ctx, name) + if err != nil { + return nil, fmt.Errorf("failed to search for server %s: %w", name, err) + } + + // Find exact match + for _, server := range servers { + // Extract simple name from reverse-DNS format + simpleName := extractAPIServerName(server.Name) + if simpleName == name || server.Name == name { + return ConvertServerJSON(server) + } + } + + return nil, fmt.Errorf("server %s not found in API", name) +} + +// SearchServers searches for servers matching the query (queries API directly) +func (p *APIRegistryProvider) SearchServers(query string) ([]ServerMetadata, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Search via API + servers, err := p.client.SearchServers(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to search servers: %w", err) + } + + return ConvertServersToMetadata(servers) +} + +// ListServers returns all servers from the API +func (p *APIRegistryProvider) ListServers() ([]ServerMetadata, error) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + servers, err := p.client.ListServers(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to list servers: %w", err) + } + + return ConvertServersToMetadata(servers) +} + +// extractAPIServerName extracts the simple name from a reverse-DNS format name +// e.g., "io.github.user/weather" -> "weather" +func extractAPIServerName(reverseDNSName string) string { + // Find the last slash + idx := strings.LastIndex(reverseDNSName, "/") + if idx == -1 { + return reverseDNSName + } + return reverseDNSName[idx+1:] +} + +// ConvertServerJSON converts an MCP Registry API ServerJSON to ToolHive ServerMetadata +func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { + if serverJSON == nil { + return nil, fmt.Errorf("serverJSON is nil") + } + + // Determine if this is a remote server or container-based server + // Remote servers have the 'remotes' field populated + // Container servers have the 'packages' field populated + if len(serverJSON.Remotes) > 0 { + return convertAPIToRemoteServer(serverJSON) + } + + // Check if server has packages + if len(serverJSON.Packages) == 0 { + // Skip servers without packages or remotes (incomplete entries) + return nil, fmt.Errorf("server %s has no packages or remotes, skipping", serverJSON.Name) + } + + return convertAPIToImageMetadata(serverJSON) +} + +// convertAPIToRemoteServer converts a ServerJSON with remotes to RemoteServerMetadata +func convertAPIToRemoteServer(serverJSON *v0.ServerJSON) (*RemoteServerMetadata, error) { + if len(serverJSON.Remotes) == 0 { + return nil, fmt.Errorf("no remotes found in server") + } + + remote := serverJSON.Remotes[0] // Use first remote (Transport type) + + metadata := &RemoteServerMetadata{ + BaseServerMetadata: BaseServerMetadata{ + Name: extractAPIServerName(serverJSON.Name), + Description: serverJSON.Description, + Tier: "Community", // Default tier + Status: "active", // Default status + Transport: convertAPITransport(remote), + Tools: []string{}, + RepositoryURL: getAPIRepositoryURL(serverJSON.Repository), + Tags: []string{}, + }, + URL: remote.URL, + Headers: convertAPIHeaders(remote.Headers), + } + + return metadata, nil +} + +// convertAPIToImageMetadata converts a ServerJSON with packages to ImageMetadata +func convertAPIToImageMetadata(serverJSON *v0.ServerJSON) (*ImageMetadata, error) { + if len(serverJSON.Packages) == 0 { + return nil, fmt.Errorf("no packages found in server") + } + + // Find OCI package + var ociPackage *model.Package + for i := range serverJSON.Packages { + pkg := &serverJSON.Packages[i] + if pkg.RegistryType == "oci" || pkg.RegistryType == "docker" { + ociPackage = pkg + break + } + } + + if ociPackage == nil { + // Fall back to first package + ociPackage = &serverJSON.Packages[0] + } + + image := formatAPIImageName(ociPackage) + + metadata := &ImageMetadata{ + BaseServerMetadata: BaseServerMetadata{ + Name: extractAPIServerName(serverJSON.Name), + Description: serverJSON.Description, + Tier: "Community", // Default tier + Status: "active", // Default status + Transport: "stdio", // Default transport for container servers + Tools: []string{}, + RepositoryURL: getAPIRepositoryURL(serverJSON.Repository), + Tags: []string{}, + }, + Image: image, + } + + return metadata, nil +} + +// ConvertServersToMetadata converts a slice of ServerJSON to a slice of ServerMetadata +// Skips servers that cannot be converted (e.g., incomplete entries) +func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]ServerMetadata, error) { + result := make([]ServerMetadata, 0, len(servers)) + + for _, server := range servers { + metadata, err := ConvertServerJSON(server) + if err != nil { + // Skip servers that can't be converted (e.g., missing packages/remotes) + // Log the error but continue processing other servers + continue + } + result = append(result, metadata) + } + + return result, nil +} + +// Helper functions for conversion + +func convertAPITransport(transport model.Transport) string { + switch transport.Type { + case "sse": + return "sse" + case "streamable-http": + return "streamable-http" + default: + return "stdio" + } +} + +func getAPIRepositoryURL(repo *model.Repository) string { + if repo != nil { + return repo.URL + } + return "" +} + +func convertAPIHeaders(headers []model.KeyValueInput) []*Header { + result := make([]*Header, 0, len(headers)) + for _, h := range headers { + result = append(result, &Header{ + Name: h.Name, + Description: h.Description, + Required: h.IsRequired, + Secret: h.IsSecret, + }) + } + return result +} + +func formatAPIImageName(pkg *model.Package) string { + switch pkg.RegistryType { + case "docker", "oci": + // For OCI, the Identifier already contains the full image reference + return pkg.Identifier + case "npm": + if pkg.Version != "" { + return fmt.Sprintf("npx://%s@%s", pkg.Identifier, pkg.Version) + } + return fmt.Sprintf("npx://%s", pkg.Identifier) + case "pypi": + if pkg.Version != "" { + return fmt.Sprintf("uvx://%s@%s", pkg.Identifier, pkg.Version) + } + return fmt.Sprintf("uvx://%s", pkg.Identifier) + default: + return pkg.Identifier + } +} From 201c3a9cd3bc5a4fedef160b500d273f5efdcb76 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 31 Oct 2025 01:10:56 +0200 Subject: [PATCH 02/20] Temporarily copy the converters from toolhive-registry Signed-off-by: Radoslav Dimitrov --- pkg/registry/api/client.go | 4 +- pkg/registry/converters.go | 388 +++++++++++++++++++++++++++++++++++ pkg/registry/provider_api.go | 146 +------------ 3 files changed, 402 insertions(+), 136 deletions(-) create mode 100644 pkg/registry/converters.go diff --git a/pkg/registry/api/client.go b/pkg/registry/api/client.go index 17eb2662b..cf612a9a9 100644 --- a/pkg/registry/api/client.go +++ b/pkg/registry/api/client.go @@ -139,7 +139,9 @@ func (c *mcpRegistryClient) ListServers(ctx context.Context, opts *ListOptions) } // fetchServersPage fetches a single page of servers -func (c *mcpRegistryClient) fetchServersPage(ctx context.Context, cursor string, opts *ListOptions) ([]*v0.ServerJSON, string, error) { +func (c *mcpRegistryClient) fetchServersPage( + ctx context.Context, cursor string, opts *ListOptions, +) ([]*v0.ServerJSON, string, error) { endpoint := fmt.Sprintf("%s/v0/servers", c.baseURL) // Build query parameters diff --git a/pkg/registry/converters.go b/pkg/registry/converters.go new file mode 100644 index 000000000..7ec537a5e --- /dev/null +++ b/pkg/registry/converters.go @@ -0,0 +1,388 @@ +// Package registry provides conversion functions from upstream MCP ServerJSON format +// to toolhive ImageMetadata/RemoteServerMetadata formats. +// +// TEMPORARY COPY: This file contains converters copied from github.com/stacklok/toolhive-registry +// to avoid circular dependency issues. This is a temporary solution. +// +// TODO: Refactor to move types to toolhive-registry and import converters from there. +// See: https://github.com/stacklok/toolhive/issues/XXXX +package registry + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + + upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + + "github.com/stacklok/toolhive/pkg/permissions" +) + +// ServerJSONToImageMetadata converts an upstream ServerJSON (with OCI packages) to toolhive ImageMetadata +// This function only handles OCI packages and will error if there are multiple OCI packages +func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*ImageMetadata, error) { + if serverJSON == nil { + return nil, fmt.Errorf("serverJSON cannot be nil") + } + + pkg, err := extractSingleOCIPackage(serverJSON) + if err != nil { + return nil, err + } + + imageMetadata := &ImageMetadata{ + BaseServerMetadata: BaseServerMetadata{ + Name: serverJSON.Title, + Description: serverJSON.Description, + Transport: pkg.Transport.Type, + }, + Image: pkg.Identifier, // OCI packages store full image ref in Identifier + } + + // Set repository URL + if serverJSON.Repository != nil && serverJSON.Repository.URL != "" { + imageMetadata.RepositoryURL = serverJSON.Repository.URL + } + + // Convert environment variables + imageMetadata.EnvVars = convertMCPEnvironmentVariables(pkg.EnvironmentVariables) + + // Extract target port from transport URL if present + imageMetadata.TargetPort = extractTargetPort(pkg.Transport.URL, serverJSON.Name) + + // Convert PackageArguments to simple Args (priority: structured arguments first) + if len(pkg.PackageArguments) > 0 { + imageMetadata.Args = flattenPackageArguments(pkg.PackageArguments) + } + + // Extract publisher-provided extensions (including Args fallback) + extractImageExtensions(serverJSON, imageMetadata) + + return imageMetadata, nil +} + +// extractSingleOCIPackage validates and extracts the single OCI package from ServerJSON +func extractSingleOCIPackage(serverJSON *upstream.ServerJSON) (model.Package, error) { + if len(serverJSON.Packages) == 0 { + return model.Package{}, fmt.Errorf("server '%s' has no packages (not a container-based server)", serverJSON.Name) + } + + // Filter for OCI packages only + var ociPackages []model.Package + var packageTypes []string + for _, pkg := range serverJSON.Packages { + if pkg.RegistryType == model.RegistryTypeOCI { + ociPackages = append(ociPackages, pkg) + } + packageTypes = append(packageTypes, string(pkg.RegistryType)) + } + + if len(ociPackages) == 0 { + return model.Package{}, fmt.Errorf("server '%s' has no OCI packages (found: %v)", serverJSON.Name, packageTypes) + } + + if len(ociPackages) > 1 { + return model.Package{}, fmt.Errorf("server '%s' has %d OCI packages, expected exactly 1", serverJSON.Name, len(ociPackages)) + } + + return ociPackages[0], nil +} + +// convertMCPEnvironmentVariables converts model.KeyValueInput to EnvVar (from MCP Registry API) +func convertMCPEnvironmentVariables(envVars []model.KeyValueInput) []*EnvVar { + if len(envVars) == 0 { + return nil + } + + result := make([]*EnvVar, 0, len(envVars)) + for _, envVar := range envVars { + result = append(result, &EnvVar{ + Name: envVar.Name, + Description: envVar.Description, + Required: envVar.IsRequired, + Secret: envVar.IsSecret, + Default: envVar.Default, + }) + } + return result +} + +// extractTargetPort extracts the port number from a transport URL +func extractTargetPort(transportURL, serverName string) int { + if transportURL == "" { + return 0 + } + + parsedURL, err := url.Parse(transportURL) + if err != nil { + fmt.Printf("⚠️ Failed to parse transport URL '%s' for server '%s': %v\n", + transportURL, serverName, err) + return 0 + } + + if parsedURL.Port() == "" { + return 0 + } + + port, err := strconv.Atoi(parsedURL.Port()) + if err != nil { + fmt.Printf("⚠️ Failed to parse port from URL '%s' for server '%s': %v\n", + transportURL, serverName, err) + return 0 + } + + return port +} + +// ServerJSONToRemoteServerMetadata converts an upstream ServerJSON (with remotes) to toolhive RemoteServerMetadata +// This function extracts remote server data and reconstructs RemoteServerMetadata format +func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteServerMetadata, error) { + if serverJSON == nil { + return nil, fmt.Errorf("serverJSON cannot be nil") + } + + if len(serverJSON.Remotes) == 0 { + return nil, fmt.Errorf("server '%s' has no remotes (not a remote server)", serverJSON.Name) + } + + remote := serverJSON.Remotes[0] // Use first remote + + remoteMetadata := &RemoteServerMetadata{ + BaseServerMetadata: BaseServerMetadata{ + Name: serverJSON.Title, + Description: serverJSON.Description, + Transport: remote.Type, + }, + URL: remote.URL, + } + + // Set repository URL + if serverJSON.Repository != nil && serverJSON.Repository.URL != "" { + remoteMetadata.RepositoryURL = serverJSON.Repository.URL + } + + // Convert headers + if len(remote.Headers) > 0 { + remoteMetadata.Headers = make([]*Header, 0, len(remote.Headers)) + for _, header := range remote.Headers { + remoteMetadata.Headers = append(remoteMetadata.Headers, &Header{ + Name: header.Name, + Description: header.Description, + Required: header.IsRequired, + Secret: header.IsSecret, + }) + } + } + + // Extract publisher-provided extensions + extractRemoteExtensions(serverJSON, remoteMetadata) + + return remoteMetadata, nil +} + +// extractImageExtensions extracts publisher-provided extensions into ImageMetadata +func extractImageExtensions(serverJSON *upstream.ServerJSON, imageMetadata *ImageMetadata) { + extensions := getStacklokExtensions(serverJSON) + if extensions == nil { + return + } + + extractBasicImageFields(extensions, imageMetadata) + extractImageMetadataField(extensions, imageMetadata) + extractComplexImageFields(extensions, imageMetadata) +} + +// getStacklokExtensions retrieves the first stacklok extension data from ServerJSON +func getStacklokExtensions(serverJSON *upstream.ServerJSON) map[string]interface{} { + if serverJSON.Meta == nil || serverJSON.Meta.PublisherProvided == nil { + return nil + } + + stacklokData, ok := serverJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + if !ok { + return nil + } + + // Return first extension data (keyed by image reference or URL) + for _, extensionsData := range stacklokData { + if extensions, ok := extensionsData.(map[string]interface{}); ok { + return extensions + } + } + return nil +} + +// extractBasicImageFields extracts basic string and slice fields +func extractBasicImageFields(extensions map[string]interface{}, imageMetadata *ImageMetadata) { + if status, ok := extensions["status"].(string); ok { + imageMetadata.Status = status + } + if tier, ok := extensions["tier"].(string); ok { + imageMetadata.Tier = tier + } + if toolsData, ok := extensions["tools"].([]interface{}); ok { + imageMetadata.Tools = interfaceSliceToStringSlice(toolsData) + } + if tagsData, ok := extensions["tags"].([]interface{}); ok { + imageMetadata.Tags = interfaceSliceToStringSlice(tagsData) + } +} + +// extractImageMetadataField extracts the metadata object (stars, pulls, last_updated) +func extractImageMetadataField(extensions map[string]interface{}, imageMetadata *ImageMetadata) { + metadataData, ok := extensions["metadata"].(map[string]interface{}) + if !ok { + return + } + + imageMetadata.Metadata = &Metadata{} + if stars, ok := metadataData["stars"].(float64); ok { + imageMetadata.Metadata.Stars = int(stars) + } + if pulls, ok := metadataData["pulls"].(float64); ok { + imageMetadata.Metadata.Pulls = int(pulls) + } + if lastUpdated, ok := metadataData["last_updated"].(string); ok { + imageMetadata.Metadata.LastUpdated = lastUpdated + } +} + +// extractComplexImageFields extracts complex fields (args, permissions, provenance) +func extractComplexImageFields(extensions map[string]interface{}, imageMetadata *ImageMetadata) { + // Extract args (fallback if PackageArguments wasn't used) + if len(imageMetadata.Args) == 0 { + if argsData, ok := extensions["args"].([]interface{}); ok { + imageMetadata.Args = interfaceSliceToStringSlice(argsData) + } + } + + // Extract permissions using JSON round-trip + if permsData, ok := extensions["permissions"]; ok { + imageMetadata.Permissions = remarshalToType[*permissions.Profile](permsData) + } + + // Extract provenance using JSON round-trip + if provData, ok := extensions["provenance"]; ok { + imageMetadata.Provenance = remarshalToType[*Provenance](provData) + } +} + +// extractRemoteExtensions extracts publisher-provided extensions into RemoteServerMetadata +func extractRemoteExtensions(serverJSON *upstream.ServerJSON, remoteMetadata *RemoteServerMetadata) { + if serverJSON.Meta == nil || serverJSON.Meta.PublisherProvided == nil { + return + } + + stacklokData, ok := serverJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + if !ok { + return + } + + // Find the extension data (keyed by URL) + for _, extensionsData := range stacklokData { + extensions, ok := extensionsData.(map[string]interface{}) + if !ok { + continue + } + + // Extract fields + if status, ok := extensions["status"].(string); ok { + remoteMetadata.Status = status + } + if tier, ok := extensions["tier"].(string); ok { + remoteMetadata.Tier = tier + } + if toolsData, ok := extensions["tools"].([]interface{}); ok { + remoteMetadata.Tools = interfaceSliceToStringSlice(toolsData) + } + if tagsData, ok := extensions["tags"].([]interface{}); ok { + remoteMetadata.Tags = interfaceSliceToStringSlice(tagsData) + } + if metadataData, ok := extensions["metadata"].(map[string]interface{}); ok { + remoteMetadata.Metadata = &Metadata{} + if stars, ok := metadataData["stars"].(float64); ok { + remoteMetadata.Metadata.Stars = int(stars) + } + if pulls, ok := metadataData["pulls"].(float64); ok { + remoteMetadata.Metadata.Pulls = int(pulls) + } + if lastUpdated, ok := metadataData["last_updated"].(string); ok { + remoteMetadata.Metadata.LastUpdated = lastUpdated + } + } + + // Extract OAuth config using JSON round-trip + if oauthData, ok := extensions["oauth_config"]; ok { + remoteMetadata.OAuthConfig = remarshalToType[*OAuthConfig](oauthData) + } + + break // Only process first entry + } +} + +// remarshalToType converts an interface{} value to a specific type using JSON marshaling +// This is useful for deserializing complex nested structures from extensions +func remarshalToType[T any](data interface{}) T { + var result T + + // Marshal to JSON + jsonData, err := json.Marshal(data) + if err != nil { + return result // Return zero value on error + } + + // Unmarshal into target type + _ = json.Unmarshal(jsonData, &result) // Ignore error, return zero value if fails + + return result +} + +// flattenPackageArguments converts structured PackageArguments to simple string Args +// This provides better interoperability when importing from upstream sources +func flattenPackageArguments(args []model.Argument) []string { + var result []string + for _, arg := range args { + // Add the argument name/flag if present + if arg.Name != "" { + result = append(result, arg.Name) + } + // Add the value if present (for named args with values or positional args) + if arg.Value != "" { + result = append(result, arg.Value) + } + } + return result +} + +// interfaceSliceToStringSlice converts []interface{} to []string +func interfaceSliceToStringSlice(input []interface{}) []string { + result := make([]string, 0, len(input)) + for _, item := range input { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result +} + +// ExtractServerName extracts the simple server name from a reverse-DNS format name +// Example: "io.github.stacklok/fetch" -> "fetch" +func ExtractServerName(reverseDNSName string) string { + parts := strings.Split(reverseDNSName, "/") + if len(parts) == 2 { + return parts[1] + } + return reverseDNSName +} + +// BuildReverseDNSName builds a reverse-DNS format name from a simple name +// Example: "fetch" -> "io.github.stacklok/fetch" +func BuildReverseDNSName(simpleName string) string { + if strings.Contains(simpleName, "/") { + return simpleName // Already in reverse-DNS format + } + return "io.github.stacklok/" + simpleName +} diff --git a/pkg/registry/provider_api.go b/pkg/registry/provider_api.go index 348ed1546..c49a99fd5 100644 --- a/pkg/registry/provider_api.go +++ b/pkg/registry/provider_api.go @@ -3,15 +3,17 @@ package registry import ( "context" "fmt" - "strings" "time" v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/modelcontextprotocol/registry/pkg/model" "github.com/stacklok/toolhive/pkg/registry/api" ) +// NOTE: Using converters from converters.go (same package) to avoid circular dependency. +// This is a TEMPORARY solution - converters are copied from toolhive-registry. +// TODO: Move types to toolhive-registry and import converters as a library. + // APIRegistryProvider provides registry data from an MCP Registry API endpoint // It queries the API on-demand for each operation, ensuring fresh data. type APIRegistryProvider struct { @@ -108,7 +110,7 @@ func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { // Find exact match for _, server := range servers { // Extract simple name from reverse-DNS format - simpleName := extractAPIServerName(server.Name) + simpleName := ExtractServerName(server.Name) if simpleName == name || server.Name == name { return ConvertServerJSON(server) } @@ -144,18 +146,9 @@ func (p *APIRegistryProvider) ListServers() ([]ServerMetadata, error) { return ConvertServersToMetadata(servers) } -// extractAPIServerName extracts the simple name from a reverse-DNS format name -// e.g., "io.github.user/weather" -> "weather" -func extractAPIServerName(reverseDNSName string) string { - // Find the last slash - idx := strings.LastIndex(reverseDNSName, "/") - if idx == -1 { - return reverseDNSName - } - return reverseDNSName[idx+1:] -} - // ConvertServerJSON converts an MCP Registry API ServerJSON to ToolHive ServerMetadata +// Uses converters from converters.go (same package) +// Note: Only handles OCI packages and remote servers, skips npm/pypi by design func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { if serverJSON == nil { return nil, fmt.Errorf("serverJSON is nil") @@ -165,7 +158,7 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { // Remote servers have the 'remotes' field populated // Container servers have the 'packages' field populated if len(serverJSON.Remotes) > 0 { - return convertAPIToRemoteServer(serverJSON) + return ServerJSONToRemoteServerMetadata(serverJSON) } // Check if server has packages @@ -174,77 +167,13 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { return nil, fmt.Errorf("server %s has no packages or remotes, skipping", serverJSON.Name) } - return convertAPIToImageMetadata(serverJSON) -} - -// convertAPIToRemoteServer converts a ServerJSON with remotes to RemoteServerMetadata -func convertAPIToRemoteServer(serverJSON *v0.ServerJSON) (*RemoteServerMetadata, error) { - if len(serverJSON.Remotes) == 0 { - return nil, fmt.Errorf("no remotes found in server") - } - - remote := serverJSON.Remotes[0] // Use first remote (Transport type) - - metadata := &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ - Name: extractAPIServerName(serverJSON.Name), - Description: serverJSON.Description, - Tier: "Community", // Default tier - Status: "active", // Default status - Transport: convertAPITransport(remote), - Tools: []string{}, - RepositoryURL: getAPIRepositoryURL(serverJSON.Repository), - Tags: []string{}, - }, - URL: remote.URL, - Headers: convertAPIHeaders(remote.Headers), - } - - return metadata, nil -} - -// convertAPIToImageMetadata converts a ServerJSON with packages to ImageMetadata -func convertAPIToImageMetadata(serverJSON *v0.ServerJSON) (*ImageMetadata, error) { - if len(serverJSON.Packages) == 0 { - return nil, fmt.Errorf("no packages found in server") - } - - // Find OCI package - var ociPackage *model.Package - for i := range serverJSON.Packages { - pkg := &serverJSON.Packages[i] - if pkg.RegistryType == "oci" || pkg.RegistryType == "docker" { - ociPackage = pkg - break - } - } - - if ociPackage == nil { - // Fall back to first package - ociPackage = &serverJSON.Packages[0] - } - - image := formatAPIImageName(ociPackage) - - metadata := &ImageMetadata{ - BaseServerMetadata: BaseServerMetadata{ - Name: extractAPIServerName(serverJSON.Name), - Description: serverJSON.Description, - Tier: "Community", // Default tier - Status: "active", // Default status - Transport: "stdio", // Default transport for container servers - Tools: []string{}, - RepositoryURL: getAPIRepositoryURL(serverJSON.Repository), - Tags: []string{}, - }, - Image: image, - } - - return metadata, nil + // ServerJSONToImageMetadata only handles OCI packages, will error on npm/pypi + return ServerJSONToImageMetadata(serverJSON) } // ConvertServersToMetadata converts a slice of ServerJSON to a slice of ServerMetadata // Skips servers that cannot be converted (e.g., incomplete entries) +// Uses official converters from toolhive-registry package func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]ServerMetadata, error) { result := make([]ServerMetadata, 0, len(servers)) @@ -260,56 +189,3 @@ func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]ServerMetadata, error return result, nil } - -// Helper functions for conversion - -func convertAPITransport(transport model.Transport) string { - switch transport.Type { - case "sse": - return "sse" - case "streamable-http": - return "streamable-http" - default: - return "stdio" - } -} - -func getAPIRepositoryURL(repo *model.Repository) string { - if repo != nil { - return repo.URL - } - return "" -} - -func convertAPIHeaders(headers []model.KeyValueInput) []*Header { - result := make([]*Header, 0, len(headers)) - for _, h := range headers { - result = append(result, &Header{ - Name: h.Name, - Description: h.Description, - Required: h.IsRequired, - Secret: h.IsSecret, - }) - } - return result -} - -func formatAPIImageName(pkg *model.Package) string { - switch pkg.RegistryType { - case "docker", "oci": - // For OCI, the Identifier already contains the full image reference - return pkg.Identifier - case "npm": - if pkg.Version != "" { - return fmt.Sprintf("npx://%s@%s", pkg.Identifier, pkg.Version) - } - return fmt.Sprintf("npx://%s", pkg.Identifier) - case "pypi": - if pkg.Version != "" { - return fmt.Sprintf("uvx://%s@%s", pkg.Identifier, pkg.Version) - } - return fmt.Sprintf("uvx://%s", pkg.Identifier) - default: - return pkg.Identifier - } -} From 65bc3f7bb27f0127d71af8bab8e4bb51f7b80e5d Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 31 Oct 2025 14:28:35 +0200 Subject: [PATCH 03/20] Complete the initial implementation Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/run.go | 13 +++---- pkg/registry/api/client.go | 18 ++++++++-- pkg/registry/converters.go | 16 +++++++-- pkg/registry/provider_api.go | 68 +++++++++++++++++++++++++++++------- 4 files changed, 92 insertions(+), 23 deletions(-) diff --git a/cmd/thv/app/run.go b/cmd/thv/app/run.go index 5cd6d37f6..078c533db 100644 --- a/cmd/thv/app/run.go +++ b/cmd/thv/app/run.go @@ -200,7 +200,7 @@ func runSingleServer(ctx context.Context, runFlags *RunFlags, serverOrImage stri } if runFlags.Name == "" { - runFlags.Name = getworkloadDefaultName(serverOrImage) + runFlags.Name = getworkloadDefaultName(ctx, serverOrImage) logger.Infof("No workload name specified, using generated name: %s", runFlags.Name) } exists, err := workloadManager.DoesWorkloadExist(ctx, runFlags.Name) @@ -258,7 +258,7 @@ func deriveRemoteName(remoteURL string) (string, error) { // getworkloadDefaultName generates a default workload name based on the serverOrImage input // This function reuses the existing system's naming logic to ensure consistency -func getworkloadDefaultName(serverOrImage string) string { +func getworkloadDefaultName(ctx context.Context, serverOrImage string) string { // If it's a protocol scheme (uvx://, npx://, go://) if runner.IsImageProtocolScheme(serverOrImage) { // Extract package name from protocol scheme using the existing parseProtocolScheme logic @@ -281,10 +281,11 @@ func getworkloadDefaultName(serverOrImage string) string { } // Check if it's a server name from registry - // Registry server names are typically multi-word names with hyphens - if !strings.Contains(serverOrImage, "://") && !strings.Contains(serverOrImage, "/") && !strings.Contains(serverOrImage, ":") { - // Likely a registry server name (no protocol, no slashes, no colons), return as-is - return serverOrImage + if !strings.Contains(serverOrImage, "://") && !strings.Contains(serverOrImage, ":") { + // Simple server name (no protocol, no slashes, no colons), return as-is + if !strings.Contains(serverOrImage, "/") { + return serverOrImage + } } // For container images, use the existing container.GetOrGenerateContainerName logic diff --git a/pkg/registry/api/client.go b/pkg/registry/api/client.go index cf612a9a9..43adf347d 100644 --- a/pkg/registry/api/client.go +++ b/pkg/registry/api/client.go @@ -24,6 +24,7 @@ type Client interface { ListServers(ctx context.Context, opts *ListOptions) ([]*v0.ServerJSON, error) // SearchServers searches for servers matching the query string + // Always returns the latest version of each server SearchServers(ctx context.Context, query string) ([]*v0.ServerJSON, error) // ValidateEndpoint validates that the endpoint implements the MCP Registry API @@ -72,10 +73,11 @@ func NewClient(baseURL string, allowPrivateIp bool) (Client, error) { } // GetServer retrieves a single server by its reverse-DNS name +// Always returns the latest version func (c *mcpRegistryClient) GetServer(ctx context.Context, name string) (*v0.ServerJSON, error) { // URL encode the server name to handle special characters encodedName := url.PathEscape(name) - endpoint := fmt.Sprintf("%s/v0/servers/%s", c.baseURL, encodedName) + endpoint := fmt.Sprintf("%s/v0/servers/%s/versions/latest", c.baseURL, encodedName) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { @@ -102,13 +104,17 @@ func (c *mcpRegistryClient) GetServer(ctx context.Context, name string) (*v0.Ser } // ListServers retrieves all servers with automatic pagination handling +// Defaults to returning only the latest version of each server func (c *mcpRegistryClient) ListServers(ctx context.Context, opts *ListOptions) ([]*v0.ServerJSON, error) { if opts == nil { - opts = &ListOptions{Limit: 100} + opts = &ListOptions{Limit: 100, Version: "latest"} } if opts.Limit == 0 { opts.Limit = 100 } + if opts.Version == "" { + opts.Version = "latest" + } var allServers []*v0.ServerJSON cursor := "" @@ -194,8 +200,14 @@ func (c *mcpRegistryClient) fetchServersPage( } // SearchServers searches for servers matching the query string +// Always returns the latest version of each server func (c *mcpRegistryClient) SearchServers(ctx context.Context, query string) ([]*v0.ServerJSON, error) { - endpoint := fmt.Sprintf("%s/v0/servers?search=%s", c.baseURL, url.QueryEscape(query)) + // Build query parameters - always include version=latest + params := url.Values{} + params.Add("search", query) + params.Add("version", "latest") + + endpoint := fmt.Sprintf("%s/v0/servers?%s", c.baseURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { diff --git a/pkg/registry/converters.go b/pkg/registry/converters.go index 7ec537a5e..36f867736 100644 --- a/pkg/registry/converters.go +++ b/pkg/registry/converters.go @@ -33,9 +33,15 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*ImageMetadata, return nil, err } + // Use Title if present, otherwise extract from reverse-DNS Name + name := serverJSON.Title + if name == "" { + name = ExtractServerName(serverJSON.Name) + } + imageMetadata := &ImageMetadata{ BaseServerMetadata: BaseServerMetadata{ - Name: serverJSON.Title, + Name: name, Description: serverJSON.Description, Transport: pkg.Transport.Type, }, @@ -150,9 +156,15 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteS remote := serverJSON.Remotes[0] // Use first remote + // Use Title if present, otherwise extract from reverse-DNS Name + name := serverJSON.Title + if name == "" { + name = ExtractServerName(serverJSON.Name) + } + remoteMetadata := &RemoteServerMetadata{ BaseServerMetadata: BaseServerMetadata{ - Name: serverJSON.Title, + Name: name, Description: serverJSON.Description, Transport: remote.Type, }, diff --git a/pkg/registry/provider_api.go b/pkg/registry/provider_api.go index c49a99fd5..6c6dbf472 100644 --- a/pkg/registry/provider_api.go +++ b/pkg/registry/provider_api.go @@ -2,6 +2,7 @@ package registry import ( "context" + "encoding/json" "fmt" "time" @@ -100,16 +101,32 @@ func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - // Try to find server by searching (since API uses reverse-DNS names) - // First try direct lookup by assuming simple name + // Try direct API lookup first (supports both reverse-DNS and simple names) + // Build potential reverse-DNS name + reverseDNSName := BuildReverseDNSName(name) + + // Try the reverse-DNS format first + serverJSON, err := p.client.GetServer(ctx, reverseDNSName) + if err == nil { + return ConvertServerJSON(serverJSON) + } + + // If that failed and the name is already in reverse-DNS format, try as-is + if reverseDNSName != name { + serverJSON, err = p.client.GetServer(ctx, name) + if err == nil { + return ConvertServerJSON(serverJSON) + } + } + + // Fall back to search for backward compatibility servers, err := p.client.SearchServers(ctx, name) if err != nil { - return nil, fmt.Errorf("failed to search for server %s: %w", name, err) + return nil, fmt.Errorf("failed to find server %s: %w", name, err) } - // Find exact match + // Find exact match in search results for _, server := range servers { - // Extract simple name from reverse-DNS format simpleName := ExtractServerName(server.Name) if simpleName == name || server.Name == name { return ConvertServerJSON(server) @@ -146,6 +163,23 @@ func (p *APIRegistryProvider) ListServers() ([]ServerMetadata, error) { return ConvertServersToMetadata(servers) } +// GetImageServer returns a specific container server by name (overrides BaseProvider) +// This override is necessary because BaseProvider.GetImageServer calls p.GetServer, +// which would call BaseProvider.GetServer instead of APIRegistryProvider.GetServer +func (p *APIRegistryProvider) GetImageServer(name string) (*ImageMetadata, error) { + server, err := p.GetServer(name) + if err != nil { + return nil, err + } + + // Type assert to ImageMetadata + if img, ok := server.(*ImageMetadata); ok { + return img, nil + } + + return nil, fmt.Errorf("server %s is not a container server", name) +} + // ConvertServerJSON converts an MCP Registry API ServerJSON to ToolHive ServerMetadata // Uses converters from converters.go (same package) // Note: Only handles OCI packages and remote servers, skips npm/pypi by design @@ -157,18 +191,28 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { // Determine if this is a remote server or container-based server // Remote servers have the 'remotes' field populated // Container servers have the 'packages' field populated - if len(serverJSON.Remotes) > 0 { - return ServerJSONToRemoteServerMetadata(serverJSON) - } + var result ServerMetadata + var err error - // Check if server has packages - if len(serverJSON.Packages) == 0 { + if len(serverJSON.Remotes) > 0 { + result, err = ServerJSONToRemoteServerMetadata(serverJSON) + } else if len(serverJSON.Packages) == 0 { // Skip servers without packages or remotes (incomplete entries) return nil, fmt.Errorf("server %s has no packages or remotes, skipping", serverJSON.Name) + } else { + // ServerJSONToImageMetadata only handles OCI packages, will error on npm/pypi + result, err = ServerJSONToImageMetadata(serverJSON) } - // ServerJSONToImageMetadata only handles OCI packages, will error on npm/pypi - return ServerJSONToImageMetadata(serverJSON) + if err != nil { + return nil, err + } + + // TEMPORARY DEBUG: Print after conversion + afterJSON, _ := json.MarshalIndent(result, "", " ") + fmt.Printf("\n=== AFTER CONVERSION ===\n%s\n", string(afterJSON)) + + return result, nil } // ConvertServersToMetadata converts a slice of ServerJSON to a slice of ServerMetadata From 0c331c54fc590f1d152455a9709943c09a9daf8b Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 4 Nov 2025 16:47:31 +0200 Subject: [PATCH 04/20] Support setting the registry API via REST Signed-off-by: Radoslav Dimitrov --- pkg/api/v1/registry.go | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pkg/api/v1/registry.go b/pkg/api/v1/registry.go index 9ab8f5695..83e3b38a4 100644 --- a/pkg/api/v1/registry.go +++ b/pkg/api/v1/registry.go @@ -241,17 +241,19 @@ func (rr *RegistryRoutes) updateRegistry(w http.ResponseWriter, r *http.Request) return } - // Validate that only one of URL or LocalPath is provided - if req.URL != nil && req.LocalPath != nil { - http.Error(w, "Cannot specify both URL and local path", http.StatusBadRequest) + // Validate that only one of URL, APIURL, or LocalPath is provided + if (req.URL != nil && req.APIURL != nil) || + (req.URL != nil && req.LocalPath != nil) || + (req.APIURL != nil && req.LocalPath != nil) { + http.Error(w, "Cannot specify more than one registry type (url, api_url, or local_path)", http.StatusBadRequest) return } var responseType string var message string - // Handle reset to default (no URL or LocalPath specified) - if req.URL == nil && req.LocalPath == nil { + // Handle reset to default (no URL, APIURL, or LocalPath specified) + if req.URL == nil && req.APIURL == nil && req.LocalPath == nil { // Use the config provider to unset the registry provider := rr.configProvider if err := provider.UnsetRegistry(); err != nil { @@ -276,6 +278,21 @@ func (rr *RegistryRoutes) updateRegistry(w http.ResponseWriter, r *http.Request) } responseType = "url" message = fmt.Sprintf("Successfully set registry URL: %s", *req.URL) + } else if req.APIURL != nil { + // Handle API URL update + allowPrivateIP := false + if req.AllowPrivateIP != nil { + allowPrivateIP = *req.AllowPrivateIP + } + + // Use the config provider to update the registry API URL + if err := rr.configProvider.SetRegistryAPI(*req.APIURL, allowPrivateIP); err != nil { + logger.Errorf("Failed to set registry API URL: %v", err) + http.Error(w, fmt.Sprintf("Failed to set registry API URL: %v", err), http.StatusBadRequest) + return + } + responseType = "api" + message = fmt.Sprintf("Successfully set registry API URL: %s", *req.APIURL) } else if req.LocalPath != nil { // Handle local path update // Use the config provider to update the registry file @@ -521,9 +538,11 @@ type getServerResponse struct { type UpdateRegistryRequest struct { // Registry URL (for remote registries) URL *string `json:"url,omitempty"` + // MCP Registry API URL + APIURL *string `json:"api_url,omitempty"` // Local registry file path LocalPath *string `json:"local_path,omitempty"` - // Allow private IP addresses for registry URL + // Allow private IP addresses for registry URL or API URL AllowPrivateIP *bool `json:"allow_private_ip,omitempty"` } From 3c4291b72c177908893d85bfcccc1928edc7cc73 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 12:47:19 +0200 Subject: [PATCH 05/20] Move the whole converters package from toolhive-registry Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/group.go | 15 +- cmd/thv/app/registry.go | 17 +- cmd/thv/app/run_flags.go | 16 +- cmd/thv/app/search.go | 7 +- pkg/api/v1/registry.go | 19 +- pkg/api/v1/workload_service.go | 10 +- pkg/api/v1/workload_types.go | 6 +- pkg/api/v1/workloads_test.go | 4 +- pkg/container/verifier/verifier.go | 10 +- pkg/mcp/server/handler_mock_test.go | 8 +- pkg/mcp/server/handler_test.go | 20 +- pkg/mcp/server/run_server.go | 10 +- pkg/mcp/server/search_registry.go | 4 +- .../converters/converters_fixture_test.go | 258 ++++ pkg/registry/converters/converters_test.go | 1265 +++++++++++++++++ pkg/registry/converters/integration_test.go | 412 ++++++ pkg/registry/converters/testdata/README.md | 174 +++ .../image_to_server/expected_github.json | 139 ++ .../image_to_server/input_github.json | 122 ++ .../remote_to_server/expected_example.json | 53 + .../remote_to_server/input_example.json | 36 + .../server_to_image/expected_github.json | 102 ++ .../server_to_image/input_github.json | 120 ++ .../server_to_remote/expected_example.json | 36 + .../server_to_remote/input_example.json | 54 + .../converters/toolhive_to_upstream.go | 287 ++++ .../upstream_to_toolhive.go} | 103 +- pkg/registry/converters/utils.go | 36 + pkg/registry/mocks/mock_provider.go | 30 +- pkg/registry/provider.go | 16 +- pkg/registry/provider_api.go | 46 +- pkg/registry/provider_base.go | 30 +- pkg/registry/provider_local.go | 8 +- pkg/registry/provider_remote.go | 5 +- .../{types.go => types/registry_types.go} | 4 +- pkg/registry/upstream_conversion.go | 49 +- pkg/runner/config.go | 1 + pkg/runner/config_builder.go | 12 +- pkg/runner/config_builder_test.go | 17 +- pkg/runner/config_test.go | 19 +- pkg/runner/env.go | 18 +- pkg/runner/retriever/retriever.go | 29 +- 42 files changed, 3343 insertions(+), 284 deletions(-) create mode 100644 pkg/registry/converters/converters_fixture_test.go create mode 100644 pkg/registry/converters/converters_test.go create mode 100644 pkg/registry/converters/integration_test.go create mode 100644 pkg/registry/converters/testdata/README.md create mode 100644 pkg/registry/converters/testdata/image_to_server/expected_github.json create mode 100644 pkg/registry/converters/testdata/image_to_server/input_github.json create mode 100644 pkg/registry/converters/testdata/remote_to_server/expected_example.json create mode 100644 pkg/registry/converters/testdata/remote_to_server/input_example.json create mode 100644 pkg/registry/converters/testdata/server_to_image/expected_github.json create mode 100644 pkg/registry/converters/testdata/server_to_image/input_github.json create mode 100644 pkg/registry/converters/testdata/server_to_remote/expected_example.json create mode 100644 pkg/registry/converters/testdata/server_to_remote/input_example.json create mode 100644 pkg/registry/converters/toolhive_to_upstream.go rename pkg/registry/{converters.go => converters/upstream_to_toolhive.go} (78%) create mode 100644 pkg/registry/converters/utils.go rename pkg/registry/{types.go => types/registry_types.go} (99%) diff --git a/cmd/thv/app/group.go b/cmd/thv/app/group.go index 6983eb48f..902c3384b 100644 --- a/cmd/thv/app/group.go +++ b/cmd/thv/app/group.go @@ -16,6 +16,7 @@ import ( "github.com/stacklok/toolhive/pkg/core" "github.com/stacklok/toolhive/pkg/groups" "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner/retriever" "github.com/stacklok/toolhive/pkg/transport" "github.com/stacklok/toolhive/pkg/validation" @@ -422,7 +423,7 @@ func groupRunCmdFunc(cmd *cobra.Command, args []string) error { // validateGroupRunPreconditions validates all conditions before making any changes func validateGroupRunPreconditions(ctx context.Context, groupName string, - registryGroup *registry.Group, secrets []string, envVars []string) error { + registryGroup *types.Group, secrets []string, envVars []string) error { if err := validateRuntimeGroupDoesNotExist(ctx, groupName); err != nil { return err } @@ -453,7 +454,7 @@ func validateRuntimeGroupDoesNotExist(ctx context.Context, groupName string) err } // validateServersDoNotExist checks that no servers in the group already exist as workloads -func validateServersDoNotExist(ctx context.Context, registryGroup *registry.Group) error { +func validateServersDoNotExist(ctx context.Context, registryGroup *types.Group) error { rt, err := container.NewFactory().Create(ctx) if err != nil { return fmt.Errorf("failed to create container runtime: %w", err) @@ -489,7 +490,7 @@ func validateServersDoNotExist(ctx context.Context, registryGroup *registry.Grou } // validateSecretsFormat validates all secrets have correct format and target existing servers -func validateSecretsFormat(secrets []string, registryGroup *registry.Group) error { +func validateSecretsFormat(secrets []string, registryGroup *types.Group) error { for _, secret := range secrets { if err := validateSecretFormat(secret, registryGroup); err != nil { return err @@ -499,7 +500,7 @@ func validateSecretsFormat(secrets []string, registryGroup *registry.Group) erro } // validateEnvVarsFormat validates all environment variables have correct format and target existing servers -func validateEnvVarsFormat(envVars []string, registryGroup *registry.Group) error { +func validateEnvVarsFormat(envVars []string, registryGroup *types.Group) error { for _, envVar := range envVars { if err := validateEnvVarFormat(envVar, registryGroup); err != nil { return err @@ -510,7 +511,7 @@ func validateEnvVarsFormat(envVars []string, registryGroup *registry.Group) erro // deployServer deploys a single server using the same path as the normal run command func deployServer(ctx context.Context, serverName string, - serverMetadata *registry.ImageMetadata, groupName string, secrets []string, envVars []string, cmd *cobra.Command) error { + serverMetadata *types.ImageMetadata, groupName string, secrets []string, envVars []string, cmd *cobra.Command) error { // Filter secrets and env vars for this specific server serverSecrets := filterSecretsForServer(secrets, serverName) @@ -564,7 +565,7 @@ func deployRemoteServer(ctx context.Context, serverName string, } // validateSecretFormat validates secret format and checks if target server exists in the group -func validateSecretFormat(secret string, registryGroup *registry.Group) error { +func validateSecretFormat(secret string, registryGroup *types.Group) error { // Expected format: NAME,target=SERVER_NAME.TARGET parts := strings.Split(secret, ",target=") if len(parts) != 2 { @@ -603,7 +604,7 @@ func validateSecretFormat(secret string, registryGroup *registry.Group) error { } // validateEnvVarFormat validates environment variable format and checks if target server exists in the group -func validateEnvVarFormat(envVar string, registryGroup *registry.Group) error { +func validateEnvVarFormat(envVar string, registryGroup *types.Group) error { // Expected format: SERVER_NAME.KEY=VALUE parts := strings.Split(envVar, "=") if len(parts) < 2 { diff --git a/cmd/thv/app/registry.go b/cmd/thv/app/registry.go index 82c0e8efe..48b3a1b73 100644 --- a/cmd/thv/app/registry.go +++ b/cmd/thv/app/registry.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" transtypes "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -64,7 +65,7 @@ func registryListCmdFunc(_ *cobra.Command, _ []string) error { } // Sort servers by name using the utility function - registry.SortServersByName(servers) + types.SortServersByName(servers) // Output based on format switch registryFormat { @@ -99,7 +100,7 @@ func registryInfoCmdFunc(_ *cobra.Command, args []string) error { } // printJSONServers prints servers in JSON format -func printJSONServers(servers []registry.ServerMetadata) error { +func printJSONServers(servers []types.ServerMetadata) error { // Marshal to JSON jsonData, err := json.MarshalIndent(servers, "", " ") if err != nil { @@ -112,7 +113,7 @@ func printJSONServers(servers []registry.ServerMetadata) error { } // printJSONServer prints a single server in JSON format -func printJSONServer(server registry.ServerMetadata) error { +func printJSONServer(server types.ServerMetadata) error { jsonData, err := json.MarshalIndent(server, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %v", err) @@ -124,7 +125,7 @@ func printJSONServer(server registry.ServerMetadata) error { } // printTextServers prints servers in text format -func printTextServers(servers []registry.ServerMetadata) { +func printTextServers(servers []types.ServerMetadata) { // Create a tabwriter for pretty output w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tTIER\tSTARS\tPULLS") @@ -166,7 +167,7 @@ const ( ) // getServerType returns the type of server (container or remote) -func getServerType(server registry.ServerMetadata) string { +func getServerType(server types.ServerMetadata) string { if server.IsRemote() { return ServerTypeRemote } @@ -175,7 +176,7 @@ func getServerType(server registry.ServerMetadata) string { // printTextServerInfo prints detailed information about a server in text format // nolint:gocyclo -func printTextServerInfo(name string, server registry.ServerMetadata) { +func printTextServerInfo(name string, server types.ServerMetadata) { fmt.Printf("Name: %s\n", server.GetName()) fmt.Printf("Type: %s\n", getServerType(server)) fmt.Printf("Description: %s\n", server.GetDescription()) @@ -186,7 +187,7 @@ func printTextServerInfo(name string, server registry.ServerMetadata) { // Type-specific information if !server.IsRemote() { // Container server - if img, ok := server.(*registry.ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { fmt.Printf("Image: %s\n", img.Image) isHTTPTransport := img.Transport == transtypes.TransportTypeSSE.String() || img.Transport == transtypes.TransportTypeStreamableHTTP.String() @@ -240,7 +241,7 @@ func printTextServerInfo(name string, server registry.ServerMetadata) { } } else { // Remote server - if remote, ok := server.(*registry.RemoteServerMetadata); ok { + if remote, ok := server.(*types.RemoteServerMetadata); ok { fmt.Printf("URL: %s\n", remote.URL) // Print headers diff --git a/cmd/thv/app/run_flags.go b/cmd/thv/app/run_flags.go index 9d836cf5a..37683d7b8 100644 --- a/cmd/thv/app/run_flags.go +++ b/cmd/thv/app/run_flags.go @@ -20,7 +20,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/process" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/runner/retriever" "github.com/stacklok/toolhive/pkg/telemetry" @@ -358,7 +358,7 @@ func handleImageRetrieval( groupName string, ) ( string, - registry.ServerMetadata, + regtypes.ServerMetadata, error, ) { @@ -409,7 +409,7 @@ func buildRunnerConfig( validatedHost string, rt runtime.Deployer, imageURL string, - serverMetadata registry.ServerMetadata, + serverMetadata regtypes.ServerMetadata, envVars map[string]string, envVarValidator runner.EnvVarValidator, oidcConfig *auth.TokenValidatorConfig, @@ -425,7 +425,7 @@ func buildRunnerConfig( // Determine server name for telemetry (similar to validateConfig logic) // This ensures telemetry middleware gets the correct server name - imageMetadata, _ := serverMetadata.(*registry.ImageMetadata) + imageMetadata, _ := serverMetadata.(*regtypes.ImageMetadata) serverName := runFlags.Name if serverName == "" && imageMetadata != nil { serverName = imageMetadata.Name @@ -484,7 +484,7 @@ func buildRunnerConfig( // configureMiddlewareAndOptions configures middleware and additional runner options func configureMiddlewareAndOptions( runFlags *RunFlags, - serverMetadata registry.ServerMetadata, + serverMetadata regtypes.ServerMetadata, toolsOverride map[string]runner.ToolOverride, oidcConfig *auth.TokenValidatorConfig, telemetryConfig *telemetry.Config, @@ -566,10 +566,10 @@ func configureMiddlewareAndOptions( } // configureRemoteAuth configures remote authentication options if applicable -func configureRemoteAuth(runFlags *RunFlags, serverMetadata registry.ServerMetadata) ([]runner.RunConfigBuilderOption, error) { +func configureRemoteAuth(runFlags *RunFlags, serverMetadata regtypes.ServerMetadata) ([]runner.RunConfigBuilderOption, error) { var opts []runner.RunConfigBuilderOption - if remoteServerMetadata, ok := serverMetadata.(*registry.RemoteServerMetadata); ok { + if remoteServerMetadata, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok { remoteAuthConfig, err := getRemoteAuthFromRemoteServerMetadata(remoteServerMetadata, runFlags) if err != nil { return nil, err @@ -619,7 +619,7 @@ func extractTelemetryValues(config *telemetry.Config) (string, float64, []string // getRemoteAuthFromRemoteServerMetadata creates RemoteAuthConfig from RemoteServerMetadata, // giving CLI flags priority. For OAuthParams: if CLI provides any, they REPLACE metadata entirely. func getRemoteAuthFromRemoteServerMetadata( - remoteServerMetadata *registry.RemoteServerMetadata, + remoteServerMetadata *regtypes.RemoteServerMetadata, runFlags *RunFlags, ) (*remote.Config, error) { if remoteServerMetadata == nil || remoteServerMetadata.OAuthConfig == nil { diff --git a/cmd/thv/app/search.go b/cmd/thv/app/search.go index 9c6d49515..928b3fc40 100644 --- a/cmd/thv/app/search.go +++ b/cmd/thv/app/search.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" ) var searchCmd = &cobra.Command{ @@ -49,7 +50,7 @@ func searchCmdFunc(_ *cobra.Command, args []string) error { } // Sort servers by name using the utility function - registry.SortServersByName(servers) + types.SortServersByName(servers) // Output based on format switch searchFormat { @@ -63,7 +64,7 @@ func searchCmdFunc(_ *cobra.Command, args []string) error { } // printJSONSearchResults prints servers in JSON format -func printJSONSearchResults(servers []registry.ServerMetadata) error { +func printJSONSearchResults(servers []types.ServerMetadata) error { // Marshal to JSON jsonData, err := json.MarshalIndent(servers, "", " ") if err != nil { @@ -76,7 +77,7 @@ func printJSONSearchResults(servers []registry.ServerMetadata) error { } // printTextSearchResults prints servers in text format -func printTextSearchResults(servers []registry.ServerMetadata) { +func printTextSearchResults(servers []types.ServerMetadata) { // Create a tabwriter for pretty output w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tTRANSPORT\tSTARS\tPULLS") diff --git a/pkg/api/v1/registry.go b/pkg/api/v1/registry.go index 83e3b38a4..614ca7746 100644 --- a/pkg/api/v1/registry.go +++ b/pkg/api/v1/registry.go @@ -10,6 +10,7 @@ import ( "github.com/stacklok/toolhive/pkg/config" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" ) const ( @@ -380,8 +381,8 @@ func (rr *RegistryRoutes) listServers(w http.ResponseWriter, r *http.Request) { // Build response with both container and remote servers response := listServersResponse{ - Servers: make([]*registry.ImageMetadata, 0, len(reg.Servers)), - RemoteServers: make([]*registry.RemoteServerMetadata, 0, len(reg.RemoteServers)), + Servers: make([]*types.ImageMetadata, 0, len(reg.Servers)), + RemoteServers: make([]*types.RemoteServerMetadata, 0, len(reg.RemoteServers)), } // Add container servers @@ -439,14 +440,14 @@ func (rr *RegistryRoutes) getServer(w http.ResponseWriter, r *http.Request) { // Build response based on server type var response getServerResponse if server.IsRemote() { - if remote, ok := server.(*registry.RemoteServerMetadata); ok { + if remote, ok := server.(*types.RemoteServerMetadata); ok { response = getServerResponse{ RemoteServer: remote, IsRemote: true, } } } else { - if img, ok := server.(*registry.ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { response = getServerResponse{ Server: img, IsRemote: false, @@ -507,7 +508,7 @@ type getRegistryResponse struct { // Source of the registry (URL, file path, or empty string for built-in) Source string `json:"source"` // Full registry data - Registry *registry.Registry `json:"registry"` + Registry *types.Registry `json:"registry"` } // listServersResponse represents the response for listing servers in a registry @@ -515,9 +516,9 @@ type getRegistryResponse struct { // @Description Response containing a list of servers type listServersResponse struct { // List of container servers in the registry - Servers []*registry.ImageMetadata `json:"servers"` + Servers []*types.ImageMetadata `json:"servers"` // List of remote servers in the registry (if any) - RemoteServers []*registry.RemoteServerMetadata `json:"remote_servers,omitempty"` + RemoteServers []*types.RemoteServerMetadata `json:"remote_servers,omitempty"` } // getServerResponse represents the response for getting a server from a registry @@ -525,9 +526,9 @@ type listServersResponse struct { // @Description Response containing server details type getServerResponse struct { // Container server details (if it's a container server) - Server *registry.ImageMetadata `json:"server,omitempty"` + Server *types.ImageMetadata `json:"server,omitempty"` // Remote server details (if it's a remote server) - RemoteServer *registry.RemoteServerMetadata `json:"remote_server,omitempty"` + RemoteServer *types.RemoteServerMetadata `json:"remote_server,omitempty"` // Indicates if this is a remote server IsRemote bool `json:"is_remote"` } diff --git a/pkg/api/v1/workload_service.go b/pkg/api/v1/workload_service.go index d03845ae5..92b48dd9a 100644 --- a/pkg/api/v1/workload_service.go +++ b/pkg/api/v1/workload_service.go @@ -10,7 +10,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/groups" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/runner/retriever" "github.com/stacklok/toolhive/pkg/secrets" @@ -133,8 +133,8 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq var remoteAuthConfig *remote.Config var imageURL string - var imageMetadata *registry.ImageMetadata - var serverMetadata registry.ServerMetadata + var imageMetadata *regtypes.ImageMetadata + var serverMetadata regtypes.ServerMetadata if req.URL != "" { // Configure remote authentication if OAuth config is provided @@ -164,7 +164,7 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq return nil, fmt.Errorf("failed to retrieve MCP server image: %w", err) } - if remoteServerMetadata, ok := serverMetadata.(*registry.RemoteServerMetadata); ok { + if remoteServerMetadata, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok { if remoteServerMetadata.OAuthConfig != nil { // Default resource: user-provided > registry metadata > derived from remote URL resource := req.OAuthConfig.Resource @@ -196,7 +196,7 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq } } // Handle server metadata - API only supports container servers - imageMetadata, _ = serverMetadata.(*registry.ImageMetadata) + imageMetadata, _ = serverMetadata.(*regtypes.ImageMetadata) } // Build RunConfig diff --git a/pkg/api/v1/workload_types.go b/pkg/api/v1/workload_types.go index 6d1556384..338e0b34f 100644 --- a/pkg/api/v1/workload_types.go +++ b/pkg/api/v1/workload_types.go @@ -6,7 +6,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/core" "github.com/stacklok/toolhive/pkg/permissions" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/secrets" ) @@ -71,7 +71,7 @@ type updateRequest struct { // Remote server specific fields URL string `json:"url,omitempty"` OAuthConfig remoteOAuthConfig `json:"oauth_config,omitempty"` - Headers []*registry.Header `json:"headers,omitempty"` + Headers []*types.Header `json:"headers,omitempty"` } // toolOverride represents a tool override @@ -201,7 +201,7 @@ func runConfigToCreateRequest(runConfig *runner.RunConfig) *createRequest { // Get remote OAuth config from RunConfig var oAuthConfig remoteOAuthConfig - var headers []*registry.Header + var headers []*types.Header if runConfig.RemoteAuthConfig != nil { // Parse ClientSecret from CLI format to SecretParameter (for details API) var clientSecretParam *secrets.SecretParameter diff --git a/pkg/api/v1/workloads_test.go b/pkg/api/v1/workloads_test.go index ffd23d3b3..ab7b86ebb 100644 --- a/pkg/api/v1/workloads_test.go +++ b/pkg/api/v1/workloads_test.go @@ -210,7 +210,7 @@ func TestCreateWorkload(t *testing.T) { mockRetriever := makeMockRetriever(t, "test-image", "test-image", - ®istry.ImageMetadata{Image: "test-image"}, + &types.ImageMetadata{Image: "test-image"}, nil, ) @@ -403,7 +403,7 @@ func TestUpdateWorkload(t *testing.T) { mockRetriever := makeMockRetriever(t, "test-image", "test-image", - ®istry.ImageMetadata{Image: "test-image"}, + &types.ImageMetadata{Image: "test-image"}, nil, ) diff --git a/pkg/container/verifier/verifier.go b/pkg/container/verifier/verifier.go index e8f195cf8..0fe011294 100644 --- a/pkg/container/verifier/verifier.go +++ b/pkg/container/verifier/verifier.go @@ -13,7 +13,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/images" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" ) const ( @@ -39,7 +39,7 @@ type Result struct { } // New creates a new Sigstore verifier -func New(serverInfo *registry.ImageMetadata) (*Sigstore, error) { +func New(serverInfo *types.ImageMetadata) (*Sigstore, error) { // Fail the verification early if the server information is not set if serverInfo == nil || serverInfo.Provenance == nil { return nil, ErrProvenanceServerInformationNotSet @@ -130,7 +130,7 @@ func getVerifiedResults( } // VerifyServer verifies the server information for the given image reference -func (s *Sigstore) VerifyServer(imageRef string, serverInfo *registry.ImageMetadata) (bool, error) { +func (s *Sigstore) VerifyServer(imageRef string, serverInfo *types.ImageMetadata) (bool, error) { // Get the verification results for the image reference results, err := s.GetVerificationResults(imageRef) if err != nil { @@ -153,7 +153,7 @@ func (s *Sigstore) VerifyServer(imageRef string, serverInfo *registry.ImageMetad return true, nil } -func isVerificationResultMatchingServerProvenance(r *verify.VerificationResult, p *registry.Provenance) bool { +func isVerificationResultMatchingServerProvenance(r *verify.VerificationResult, p *types.Provenance) bool { if r == nil || p == nil || r.Signature == nil || r.Signature.Certificate == nil { return false } @@ -175,7 +175,7 @@ func isVerificationResultMatchingServerProvenance(r *verify.VerificationResult, } // compareBaseProperties compares the base properties of the verification result and the server provenance -func compareBaseProperties(r *verify.VerificationResult, p *registry.Provenance) bool { +func compareBaseProperties(r *verify.VerificationResult, p *types.Provenance) bool { // Extract the signer identity from the certificate siIdentity, err := signerIdentityFromCertificate(r.Signature.Certificate) if err != nil { diff --git a/pkg/mcp/server/handler_mock_test.go b/pkg/mcp/server/handler_mock_test.go index 6304f1014..ba031b6d0 100644 --- a/pkg/mcp/server/handler_mock_test.go +++ b/pkg/mcp/server/handler_mock_test.go @@ -33,7 +33,7 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { name: "successful search with results", query: "test", mockServers: []registry.ServerMetadata{ - ®istry.ImageMetadata{ + &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-server", Description: "Test server description", @@ -43,7 +43,7 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { }, Image: "test/image:latest", }, - ®istry.ImageMetadata{ + &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "another-test", Description: "Another test server", @@ -56,7 +56,7 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { m.EXPECT(). SearchServers("test"). Return([]registry.ServerMetadata{ - ®istry.ImageMetadata{ + &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-server", Description: "Test server description", @@ -66,7 +66,7 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { }, Image: "test/image:latest", }, - ®istry.ImageMetadata{ + &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "another-test", Description: "Another test server", diff --git a/pkg/mcp/server/handler_test.go b/pkg/mcp/server/handler_test.go index c6291b13e..327898365 100644 --- a/pkg/mcp/server/handler_test.go +++ b/pkg/mcp/server/handler_test.go @@ -137,7 +137,7 @@ func TestConfigureTransport(t *testing.T) { t.Parallel() tests := []struct { name string - imageMetadata *registry.ImageMetadata + imageMetadata *types.ImageMetadata expectedTransport string }{ { @@ -147,7 +147,7 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with empty transport returns SSE", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "", }, @@ -156,7 +156,7 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with stdio transport", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "stdio", }, @@ -165,7 +165,7 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with streamable-http transport", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "streamable-http", }, @@ -189,7 +189,7 @@ func TestPrepareEnvironmentVariables(t *testing.T) { t.Parallel() tests := []struct { name string - imageMetadata *registry.ImageMetadata + imageMetadata *types.ImageMetadata userEnv map[string]string expected map[string]string }{ @@ -201,7 +201,7 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with defaults, no user env", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ EnvVars: []*registry.EnvVar{ {Name: "VAR1", Default: "default1"}, {Name: "VAR2", Default: "default2"}, @@ -215,7 +215,7 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with defaults, user overrides", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ EnvVars: []*registry.EnvVar{ {Name: "VAR1", Default: "default1"}, {Name: "VAR2", Default: "default2"}, @@ -243,7 +243,7 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with empty defaults ignored", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ EnvVars: []*registry.EnvVar{ {Name: "VAR1", Default: ""}, {Name: "VAR2", Default: "value2"}, @@ -281,7 +281,7 @@ func TestBuildServerConfig(t *testing.T) { tests := []struct { name string imageURL string - imageMetadata *registry.ImageMetadata + imageMetadata *types.ImageMetadata expectError bool }{ { @@ -293,7 +293,7 @@ func TestBuildServerConfig(t *testing.T) { { name: "valid config with metadata", imageURL: "test/image:latest", - imageMetadata: ®istry.ImageMetadata{ + imageMetadata: &types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "stdio", }, diff --git a/pkg/mcp/server/run_server.go b/pkg/mcp/server/run_server.go index 7bbbd80f1..53eb55837 100644 --- a/pkg/mcp/server/run_server.go +++ b/pkg/mcp/server/run_server.go @@ -8,7 +8,7 @@ import ( "github.com/stacklok/toolhive/pkg/container" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/runner/retriever" transporttypes "github.com/stacklok/toolhive/pkg/transport/types" @@ -48,7 +48,7 @@ func (h *Handler) RunServer(ctx context.Context, request mcp.CallToolRequest) (* } // Build run configuration - imageMetadata, _ := serverMetadata.(*registry.ImageMetadata) + imageMetadata, _ := serverMetadata.(*types.ImageMetadata) runConfig, err := buildServerConfig(ctx, args, imageURL, imageMetadata) if err != nil { @@ -107,7 +107,7 @@ func buildServerConfig( ctx context.Context, args *runServerArgs, imageURL string, - imageMetadata *registry.ImageMetadata, + imageMetadata *types.ImageMetadata, ) (*runner.RunConfig, error) { // Create container runtime rt, err := container.NewFactory().Create(ctx) @@ -141,7 +141,7 @@ func buildServerConfig( } // configureTransport sets up transport configuration from metadata -func configureTransport(opts *[]runner.RunConfigBuilderOption, imageMetadata *registry.ImageMetadata) string { +func configureTransport(opts *[]runner.RunConfigBuilderOption, imageMetadata *types.ImageMetadata) string { transport := transporttypes.TransportTypeSSE.String() if imageMetadata != nil { @@ -155,7 +155,7 @@ func configureTransport(opts *[]runner.RunConfigBuilderOption, imageMetadata *re } // prepareEnvironmentVariables merges default and user environment variables -func prepareEnvironmentVariables(imageMetadata *registry.ImageMetadata, userEnv map[string]string) map[string]string { +func prepareEnvironmentVariables(imageMetadata *types.ImageMetadata, userEnv map[string]string) map[string]string { envVarsMap := make(map[string]string) // Add default environment variables from metadata diff --git a/pkg/mcp/server/search_registry.go b/pkg/mcp/server/search_registry.go index 008377468..13bf5d726 100644 --- a/pkg/mcp/server/search_registry.go +++ b/pkg/mcp/server/search_registry.go @@ -6,7 +6,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" ) // searchRegistryArgs holds the arguments for searching the registry @@ -54,7 +54,7 @@ func (h *Handler) SearchRegistry(_ context.Context, request mcp.CallToolRequest) } // Add image-specific fields if it's an ImageMetadata - if imgMeta, ok := srv.(*registry.ImageMetadata); ok { + if imgMeta, ok := srv.(*types.ImageMetadata); ok { info.Image = imgMeta.Image info.Args = imgMeta.Args info.Tools = imgMeta.Tools diff --git a/pkg/registry/converters/converters_fixture_test.go b/pkg/registry/converters/converters_fixture_test.go new file mode 100644 index 000000000..2c15875eb --- /dev/null +++ b/pkg/registry/converters/converters_fixture_test.go @@ -0,0 +1,258 @@ +package converters + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/stacklok/toolhive/pkg/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestConverters_Fixtures validates converter functions using JSON fixture files +// This provides a clear, maintainable way to test conversions with real-world data +func TestConverters_Fixtures(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fixtureDir string + inputFile string + expectedFile string + serverName string + convertFunc string // "ImageToServer", "ServerToImage", "RemoteToServer", "ServerToRemote" + validateFunc func(t *testing.T, input, output []byte) + }{ + { + name: "ImageMetadata to ServerJSON - GitHub", + fixtureDir: "testdata/image_to_server", + inputFile: "input_github.json", + expectedFile: "expected_github.json", + serverName: "github", + convertFunc: "ImageToServer", + validateFunc: validateImageToServerConversion, + }, + { + name: "ServerJSON to ImageMetadata - GitHub", + fixtureDir: "testdata/server_to_image", + inputFile: "input_github.json", + expectedFile: "expected_github.json", + serverName: "", + convertFunc: "ServerToImage", + validateFunc: validateServerToImageConversion, + }, + { + name: "RemoteServerMetadata to ServerJSON - Example", + fixtureDir: "testdata/remote_to_server", + inputFile: "input_example.json", + expectedFile: "expected_example.json", + serverName: "example-remote", + convertFunc: "RemoteToServer", + validateFunc: validateRemoteToServerConversion, + }, + { + name: "ServerJSON to RemoteServerMetadata - Example", + fixtureDir: "testdata/server_to_remote", + inputFile: "input_example.json", + expectedFile: "expected_example.json", + serverName: "", + convertFunc: "ServerToRemote", + validateFunc: validateServerToRemoteConversion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Read input fixture + inputPath := filepath.Join(tt.fixtureDir, tt.inputFile) + inputData, err := os.ReadFile(inputPath) + require.NoError(t, err, "Failed to read input fixture: %s", inputPath) + + // Read expected output fixture + expectedPath := filepath.Join(tt.fixtureDir, tt.expectedFile) + expectedData, err := os.ReadFile(expectedPath) + require.NoError(t, err, "Failed to read expected fixture: %s", expectedPath) + + // Perform conversion based on type + var actualData []byte + switch tt.convertFunc { + case "ImageToServer": + actualData = convertImageToServer(t, inputData, tt.serverName) + case "ServerToImage": + actualData = convertServerToImage(t, inputData) + case "RemoteToServer": + actualData = convertRemoteToServer(t, inputData, tt.serverName) + case "ServerToRemote": + actualData = convertServerToRemote(t, inputData) + default: + t.Fatalf("Unknown conversion function: %s", tt.convertFunc) + } + + // Compare output with expected + var expected, actual interface{} + require.NoError(t, json.Unmarshal(expectedData, &expected), "Failed to parse expected JSON") + require.NoError(t, json.Unmarshal(actualData, &actual), "Failed to parse actual JSON") + + // Deep equal comparison + assert.Equal(t, expected, actual, "Conversion output doesn't match expected fixture") + + // Run additional validation if provided + if tt.validateFunc != nil { + tt.validateFunc(t, inputData, actualData) + } + }) + } +} + +// Helper functions for conversions + +func convertImageToServer(t *testing.T, inputData []byte, serverName string) []byte { + t.Helper() + var imageMetadata registry.ImageMetadata + require.NoError(t, json.Unmarshal(inputData, &imageMetadata)) + + serverJSON, err := ImageMetadataToServerJSON(serverName, &imageMetadata) + require.NoError(t, err) + + output, err := json.MarshalIndent(serverJSON, "", " ") + require.NoError(t, err) + return output +} + +func convertServerToImage(t *testing.T, inputData []byte) []byte { + t.Helper() + var serverJSON upstream.ServerJSON + require.NoError(t, json.Unmarshal(inputData, &serverJSON)) + + imageMetadata, err := ServerJSONToImageMetadata(&serverJSON) + require.NoError(t, err) + + output, err := json.MarshalIndent(imageMetadata, "", " ") + require.NoError(t, err) + return output +} + +func convertRemoteToServer(t *testing.T, inputData []byte, serverName string) []byte { + t.Helper() + var remoteMetadata registry.RemoteServerMetadata + require.NoError(t, json.Unmarshal(inputData, &remoteMetadata)) + + serverJSON, err := RemoteServerMetadataToServerJSON(serverName, &remoteMetadata) + require.NoError(t, err) + + output, err := json.MarshalIndent(serverJSON, "", " ") + require.NoError(t, err) + return output +} + +func convertServerToRemote(t *testing.T, inputData []byte) []byte { + t.Helper() + var serverJSON upstream.ServerJSON + require.NoError(t, json.Unmarshal(inputData, &serverJSON)) + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(&serverJSON) + require.NoError(t, err) + + output, err := json.MarshalIndent(remoteMetadata, "", " ") + require.NoError(t, err) + return output +} + +// Validation functions - additional checks beyond JSON equality + +func validateImageToServerConversion(t *testing.T, inputData, outputData []byte) { + t.Helper() + var input registry.ImageMetadata + var output upstream.ServerJSON + + require.NoError(t, json.Unmarshal(inputData, &input)) + require.NoError(t, json.Unmarshal(outputData, &output)) + + // Verify core mappings + assert.Equal(t, input.Description, output.Description, "Description should match") + assert.Len(t, output.Packages, 1, "Should have exactly one package") + assert.Equal(t, input.Image, output.Packages[0].Identifier, "Image identifier should match") + assert.Equal(t, input.Transport, output.Packages[0].Transport.Type, "Transport type should match") + + // Verify environment variables count + assert.Len(t, output.Packages[0].EnvironmentVariables, len(input.EnvVars), + "Environment variables count should match") + + // Verify publisher extensions exist + require.NotNil(t, output.Meta, "Meta should not be nil") + require.NotNil(t, output.Meta.PublisherProvided, "PublisherProvided should not be nil") + + stacklokData, ok := output.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + require.True(t, ok, "Should have io.github.stacklok namespace") + + extensions, ok := stacklokData[input.Image].(map[string]interface{}) + require.True(t, ok, "Should have image-specific extensions") + + // Verify key extension fields + assert.Equal(t, input.Status, extensions["status"], "Status should be in extensions") + assert.Equal(t, input.Tier, extensions["tier"], "Tier should be in extensions") + assert.NotNil(t, extensions["tools"], "Tools should be in extensions") + assert.NotNil(t, extensions["tags"], "Tags should be in extensions") +} + +func validateServerToImageConversion(t *testing.T, inputData, outputData []byte) { + t.Helper() + var input upstream.ServerJSON + var output registry.ImageMetadata + + require.NoError(t, json.Unmarshal(inputData, &input)) + require.NoError(t, json.Unmarshal(outputData, &output)) + + // Verify core mappings + assert.Equal(t, input.Description, output.Description, "Description should match") + require.Len(t, input.Packages, 1, "Input should have exactly one package") + assert.Equal(t, input.Packages[0].Identifier, output.Image, "Image identifier should match") + assert.Equal(t, input.Packages[0].Transport.Type, output.Transport, "Transport type should match") + + // Verify environment variables were extracted + assert.Len(t, output.EnvVars, len(input.Packages[0].EnvironmentVariables), + "Environment variables count should match") +} + +func validateRemoteToServerConversion(t *testing.T, inputData, outputData []byte) { + t.Helper() + var input registry.RemoteServerMetadata + var output upstream.ServerJSON + + require.NoError(t, json.Unmarshal(inputData, &input)) + require.NoError(t, json.Unmarshal(outputData, &output)) + + // Verify core mappings + assert.Equal(t, input.Description, output.Description, "Description should match") + require.Len(t, output.Remotes, 1, "Should have exactly one remote") + assert.Equal(t, input.URL, output.Remotes[0].URL, "Remote URL should match") + assert.Equal(t, input.Transport, output.Remotes[0].Type, "Transport type should match") + + // Verify headers count + assert.Len(t, output.Remotes[0].Headers, len(input.Headers), + "Headers count should match") +} + +func validateServerToRemoteConversion(t *testing.T, inputData, outputData []byte) { + t.Helper() + var input upstream.ServerJSON + var output registry.RemoteServerMetadata + + require.NoError(t, json.Unmarshal(inputData, &input)) + require.NoError(t, json.Unmarshal(outputData, &output)) + + // Verify core mappings + assert.Equal(t, input.Description, output.Description, "Description should match") + require.Len(t, input.Remotes, 1, "Input should have exactly one remote") + assert.Equal(t, input.Remotes[0].URL, output.URL, "Remote URL should match") + assert.Equal(t, input.Remotes[0].Type, output.Transport, "Transport type should match") + + // Verify headers were extracted + assert.Len(t, output.Headers, len(input.Remotes[0].Headers), + "Headers count should match") +} diff --git a/pkg/registry/converters/converters_test.go b/pkg/registry/converters/converters_test.go new file mode 100644 index 000000000..65c580f46 --- /dev/null +++ b/pkg/registry/converters/converters_test.go @@ -0,0 +1,1265 @@ +package converters + +import ( + "encoding/json" + "testing" + + upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stacklok/toolhive/pkg/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Helpers + +// createTestServerJSON creates a valid ServerJSON for testing with OCI package +func createTestServerJSON() *upstream.ServerJSON { + return &upstream.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "io.github.stacklok/test-server", + Description: "Test MCP server", + Version: "1.0.0", + Repository: &model.Repository{ + URL: "https://github.com/test/repo", + Source: "github", + }, + Packages: []model.Package{ + { + RegistryType: model.RegistryTypeOCI, + Identifier: "ghcr.io/test/server:latest", + Transport: model.Transport{ + Type: model.TransportTypeStdio, + }, + }, + }, + Meta: &upstream.ServerMeta{ + PublisherProvided: map[string]interface{}{ + "io.github.stacklok": map[string]interface{}{ + "ghcr.io/test/server:latest": map[string]interface{}{ + "status": "active", + "tier": "Official", + "tools": []interface{}{"tool1", "tool2"}, + "tags": []interface{}{"test", "example"}, + "metadata": map[string]interface{}{ + "stars": float64(100), + "pulls": float64(1000), + "last_updated": "2025-01-01", + }, + }, + }, + }, + }, + } +} + +// createTestImageMetadata creates a valid ImageMetadata for testing +func createTestImageMetadata() *registry.ImageMetadata { + return ®istry.ImageMetadata{ + BaseServerMetadata: registry.BaseServerMetadata{ + Description: "Test MCP server", + Transport: model.TransportTypeStdio, + RepositoryURL: "https://github.com/test/repo", + Status: "active", + Tier: "Official", + Tools: []string{"tool1", "tool2"}, + Tags: []string{"test", "example"}, + Metadata: ®istry.Metadata{ + Stars: 100, + Pulls: 1000, + LastUpdated: "2025-01-01", + }, + }, + Image: "ghcr.io/test/server:latest", + } +} + +// createTestRemoteServerMetadata creates a valid RemoteServerMetadata for testing +func createTestRemoteServerMetadata() *registry.RemoteServerMetadata { + return ®istry.RemoteServerMetadata{ + BaseServerMetadata: registry.BaseServerMetadata{ + Description: "Test remote server", + Transport: "sse", + RepositoryURL: "https://github.com/test/remote", + Status: "active", + Tier: "Official", + Tools: []string{"tool1"}, + Tags: []string{"remote"}, + }, + URL: "https://api.example.com/mcp", + } +} + +// Test Suite 1: ServerJSONToImageMetadata + +func TestServerJSONToImageMetadata_Success(t *testing.T) { + t.Parallel() + + serverJSON := createTestServerJSON() + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, imageMetadata) + + assert.Equal(t, "ghcr.io/test/server:latest", imageMetadata.Image) + assert.Equal(t, "Test MCP server", imageMetadata.Description) + assert.Equal(t, model.TransportTypeStdio, imageMetadata.Transport) + assert.Equal(t, "https://github.com/test/repo", imageMetadata.RepositoryURL) + assert.Equal(t, "active", imageMetadata.Status) + assert.Equal(t, "Official", imageMetadata.Tier) + assert.Equal(t, []string{"tool1", "tool2"}, imageMetadata.Tools) + assert.Equal(t, []string{"test", "example"}, imageMetadata.Tags) + assert.NotNil(t, imageMetadata.Metadata) + assert.Equal(t, 100, imageMetadata.Metadata.Stars) + assert.Equal(t, 1000, imageMetadata.Metadata.Pulls) + assert.Equal(t, "2025-01-01", imageMetadata.Metadata.LastUpdated) +} + +func TestServerJSONToImageMetadata_NilInput(t *testing.T) { + t.Parallel() + + imageMetadata, err := ServerJSONToImageMetadata(nil) + + assert.Error(t, err) + assert.Nil(t, imageMetadata) + assert.Contains(t, err.Error(), "serverJSON cannot be nil") +} + +func TestServerJSONToImageMetadata_NoPackages(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Packages: []model.Package{}, + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + assert.Error(t, err) + assert.Nil(t, imageMetadata) + assert.Contains(t, err.Error(), "has no packages") +} + +func TestServerJSONToImageMetadata_NoOCIPackages(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Packages: []model.Package{ + { + RegistryType: "npm", + Identifier: "test-package", + }, + }, + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + assert.Error(t, err) + assert.Nil(t, imageMetadata) + assert.Contains(t, err.Error(), "has no OCI packages") +} + +func TestServerJSONToImageMetadata_MultipleOCIPackages(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Packages: []model.Package{ + { + RegistryType: model.RegistryTypeOCI, + Identifier: "image1:latest", + }, + { + RegistryType: model.RegistryTypeOCI, + Identifier: "image2:latest", + }, + }, + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + assert.Error(t, err) + assert.Nil(t, imageMetadata) + assert.Contains(t, err.Error(), "has 2 OCI packages") +} + +func TestServerJSONToImageMetadata_WithEnvVars(t *testing.T) { + t.Parallel() + + serverJSON := createTestServerJSON() + serverJSON.Packages[0].EnvironmentVariables = []model.KeyValueInput{ + { + Name: "API_KEY", + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "API Key", + IsRequired: true, + IsSecret: true, + Default: "default-key", + }, + }, + }, + { + Name: "DEBUG", + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "Debug mode", + IsRequired: false, + IsSecret: false, + Default: "false", + }, + }, + }, + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, imageMetadata) + require.Len(t, imageMetadata.EnvVars, 2) + + assert.Equal(t, "API_KEY", imageMetadata.EnvVars[0].Name) + assert.Equal(t, "API Key", imageMetadata.EnvVars[0].Description) + assert.True(t, imageMetadata.EnvVars[0].Required) + assert.True(t, imageMetadata.EnvVars[0].Secret) + assert.Equal(t, "default-key", imageMetadata.EnvVars[0].Default) + + assert.Equal(t, "DEBUG", imageMetadata.EnvVars[1].Name) + assert.Equal(t, "Debug mode", imageMetadata.EnvVars[1].Description) + assert.False(t, imageMetadata.EnvVars[1].Required) + assert.False(t, imageMetadata.EnvVars[1].Secret) + assert.Equal(t, "false", imageMetadata.EnvVars[1].Default) +} + +func TestServerJSONToImageMetadata_WithTargetPort(t *testing.T) { + t.Parallel() + + serverJSON := createTestServerJSON() + serverJSON.Packages[0].Transport = model.Transport{ + Type: model.TransportTypeStreamableHTTP, + URL: "http://localhost:9090", + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, imageMetadata) + assert.Equal(t, 9090, imageMetadata.TargetPort) +} + +func TestServerJSONToImageMetadata_InvalidPortURL(t *testing.T) { + t.Parallel() + + serverJSON := createTestServerJSON() + serverJSON.Packages[0].Transport = model.Transport{ + Type: model.TransportTypeStreamableHTTP, + URL: "not-a-valid-url", + } + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, imageMetadata) + assert.Equal(t, 0, imageMetadata.TargetPort) // Should default to 0 on parse failure +} + +func TestServerJSONToImageMetadata_MissingPublisherExtensions(t *testing.T) { + t.Parallel() + + serverJSON := createTestServerJSON() + serverJSON.Meta = nil + + imageMetadata, err := ServerJSONToImageMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, imageMetadata) + assert.Equal(t, "", imageMetadata.Status) + assert.Equal(t, "", imageMetadata.Tier) + assert.Nil(t, imageMetadata.Tools) + assert.Nil(t, imageMetadata.Tags) + assert.Nil(t, imageMetadata.Metadata) +} + +// Test Suite 2: ImageMetadataToServerJSON + +func TestImageMetadataToServerJSON_Success(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + serverJSON, err := ImageMetadataToServerJSON("test-server", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + + assert.Equal(t, model.CurrentSchemaURL, serverJSON.Schema) + assert.Equal(t, "io.github.stacklok/test-server", serverJSON.Name) + assert.Equal(t, "Test MCP server", serverJSON.Description) + assert.Equal(t, "1.0.0", serverJSON.Version) + assert.Equal(t, "https://github.com/test/repo", serverJSON.Repository.URL) + assert.Len(t, serverJSON.Packages, 1) + assert.Equal(t, model.RegistryTypeOCI, serverJSON.Packages[0].RegistryType) + assert.Equal(t, "ghcr.io/test/server:latest", serverJSON.Packages[0].Identifier) + assert.NotNil(t, serverJSON.Meta) + assert.NotNil(t, serverJSON.Meta.PublisherProvided) +} + +func TestImageMetadataToServerJSON_NilInput(t *testing.T) { + t.Parallel() + + serverJSON, err := ImageMetadataToServerJSON("test", nil) + + assert.Error(t, err) + assert.Nil(t, serverJSON) + assert.Contains(t, err.Error(), "imageMetadata cannot be nil") +} + +func TestImageMetadataToServerJSON_EmptyName(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + serverJSON, err := ImageMetadataToServerJSON("", imageMetadata) + + assert.Error(t, err) + assert.Nil(t, serverJSON) + assert.Contains(t, err.Error(), "name cannot be empty") +} + +func TestImageMetadataToServerJSON_WithEnvVars(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + imageMetadata.EnvVars = []*registry.EnvVar{ + { + Name: "API_KEY", + Description: "API Key", + Required: true, + Secret: true, + Default: "default", + }, + } + + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Packages, 1) + require.Len(t, serverJSON.Packages[0].EnvironmentVariables, 1) + + envVar := serverJSON.Packages[0].EnvironmentVariables[0] + assert.Equal(t, "API_KEY", envVar.Name) + assert.Equal(t, "API Key", envVar.Description) + assert.True(t, envVar.IsRequired) + assert.True(t, envVar.IsSecret) + assert.Equal(t, "default", envVar.Default) +} + +func TestImageMetadataToServerJSON_WithTargetPort(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + imageMetadata.Transport = model.TransportTypeStreamableHTTP + imageMetadata.TargetPort = 9090 + + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Packages, 1) + + assert.Equal(t, model.TransportTypeStreamableHTTP, serverJSON.Packages[0].Transport.Type) + assert.Equal(t, "http://localhost:9090", serverJSON.Packages[0].Transport.URL) +} + +func TestImageMetadataToServerJSON_HTTPTransportNoPort(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + imageMetadata.Transport = model.TransportTypeStreamableHTTP + imageMetadata.TargetPort = 0 // No port specified + + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Packages, 1) + + assert.Equal(t, model.TransportTypeStreamableHTTP, serverJSON.Packages[0].Transport.Type) + assert.Equal(t, "http://localhost", serverJSON.Packages[0].Transport.URL) // No port in URL +} + +func TestImageMetadataToServerJSON_StdioTransport(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + imageMetadata.Transport = model.TransportTypeStdio + + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Packages, 1) + + assert.Equal(t, model.TransportTypeStdio, serverJSON.Packages[0].Transport.Type) + assert.Empty(t, serverJSON.Packages[0].Transport.URL) +} + +func TestImageMetadataToServerJSON_EmptyTransportDefaultsToStdio(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + imageMetadata.Transport = "" + + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Packages, 1) + + assert.Equal(t, model.TransportTypeStdio, serverJSON.Packages[0].Transport.Type) +} + +func TestImageMetadataToServerJSON_WithPublisherExtensions(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + serverJSON, err := ImageMetadataToServerJSON("test", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.NotNil(t, serverJSON.Meta) + require.NotNil(t, serverJSON.Meta.PublisherProvided) + + stacklokData, ok := serverJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + require.True(t, ok) + + imageData, ok := stacklokData["ghcr.io/test/server:latest"].(map[string]interface{}) + require.True(t, ok) + + assert.Equal(t, "active", imageData["status"]) + assert.Equal(t, "Official", imageData["tier"]) +} + +func TestImageMetadataToServerJSON_ReverseDNSName(t *testing.T) { + t.Parallel() + + imageMetadata := createTestImageMetadata() + serverJSON, err := ImageMetadataToServerJSON("fetch", imageMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + assert.Equal(t, "io.github.stacklok/fetch", serverJSON.Name) +} + +// Test Suite 3: ServerJSONToRemoteServerMetadata + +func TestServerJSONToRemoteServerMetadata_Success(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "io.github.stacklok/test-remote", + Description: "Test remote server", + Repository: &model.Repository{ + URL: "https://github.com/test/remote", + }, + Remotes: []model.Transport{ + { + Type: "sse", + URL: "https://api.example.com/mcp", + }, + }, + Meta: &upstream.ServerMeta{ + PublisherProvided: map[string]interface{}{ + "io.github.stacklok": map[string]interface{}{ + "https://api.example.com/mcp": map[string]interface{}{ + "status": "active", + "tier": "Official", + "tools": []interface{}{"tool1"}, + }, + }, + }, + }, + } + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, remoteMetadata) + + assert.Equal(t, "https://api.example.com/mcp", remoteMetadata.URL) + assert.Equal(t, "Test remote server", remoteMetadata.Description) + assert.Equal(t, "sse", remoteMetadata.Transport) + assert.Equal(t, "https://github.com/test/remote", remoteMetadata.RepositoryURL) + assert.Equal(t, "active", remoteMetadata.Status) + assert.Equal(t, "Official", remoteMetadata.Tier) + assert.Equal(t, []string{"tool1"}, remoteMetadata.Tools) +} + +func TestServerJSONToRemoteServerMetadata_NilInput(t *testing.T) { + t.Parallel() + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(nil) + + assert.Error(t, err) + assert.Nil(t, remoteMetadata) + assert.Contains(t, err.Error(), "serverJSON cannot be nil") +} + +func TestServerJSONToRemoteServerMetadata_NoRemotes(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Remotes: []model.Transport{}, + } + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(serverJSON) + + assert.Error(t, err) + assert.Nil(t, remoteMetadata) + assert.Contains(t, err.Error(), "has no remotes") +} + +func TestServerJSONToRemoteServerMetadata_WithHeaders(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Description: "Test", + Remotes: []model.Transport{ + { + Type: "sse", + URL: "https://api.example.com", + Headers: []model.KeyValueInput{ + { + Name: "Authorization", + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "Auth token", + IsRequired: true, + IsSecret: true, + }, + }, + }, + }, + }, + }, + } + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, remoteMetadata) + require.Len(t, remoteMetadata.Headers, 1) + + assert.Equal(t, "Authorization", remoteMetadata.Headers[0].Name) + assert.Equal(t, "Auth token", remoteMetadata.Headers[0].Description) + assert.True(t, remoteMetadata.Headers[0].Required) + assert.True(t, remoteMetadata.Headers[0].Secret) +} + +func TestServerJSONToRemoteServerMetadata_MissingPublisherExtensions(t *testing.T) { + t.Parallel() + + serverJSON := &upstream.ServerJSON{ + Name: "test", + Description: "Test", + Remotes: []model.Transport{ + { + Type: "sse", + URL: "https://api.example.com", + }, + }, + Meta: nil, + } + + remoteMetadata, err := ServerJSONToRemoteServerMetadata(serverJSON) + + require.NoError(t, err) + require.NotNil(t, remoteMetadata) + assert.Equal(t, "", remoteMetadata.Status) + assert.Equal(t, "", remoteMetadata.Tier) +} + +// Test Suite 4: RemoteServerMetadataToServerJSON + +func TestRemoteServerMetadataToServerJSON_Success(t *testing.T) { + t.Parallel() + + remoteMetadata := createTestRemoteServerMetadata() + serverJSON, err := RemoteServerMetadataToServerJSON("test-remote", remoteMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + + assert.Equal(t, model.CurrentSchemaURL, serverJSON.Schema) + assert.Equal(t, "io.github.stacklok/test-remote", serverJSON.Name) + assert.Equal(t, "Test remote server", serverJSON.Description) + assert.Equal(t, "https://github.com/test/remote", serverJSON.Repository.URL) + assert.Len(t, serverJSON.Remotes, 1) + assert.Equal(t, "sse", serverJSON.Remotes[0].Type) + assert.Equal(t, "https://api.example.com/mcp", serverJSON.Remotes[0].URL) +} + +func TestRemoteServerMetadataToServerJSON_NilInput(t *testing.T) { + t.Parallel() + + serverJSON, err := RemoteServerMetadataToServerJSON("test", nil) + + assert.Error(t, err) + assert.Nil(t, serverJSON) + assert.Contains(t, err.Error(), "remoteMetadata cannot be nil") +} + +func TestRemoteServerMetadataToServerJSON_EmptyName(t *testing.T) { + t.Parallel() + + remoteMetadata := createTestRemoteServerMetadata() + serverJSON, err := RemoteServerMetadataToServerJSON("", remoteMetadata) + + assert.Error(t, err) + assert.Nil(t, serverJSON) + assert.Contains(t, err.Error(), "name cannot be empty") +} + +func TestRemoteServerMetadataToServerJSON_WithHeaders(t *testing.T) { + t.Parallel() + + remoteMetadata := createTestRemoteServerMetadata() + remoteMetadata.Headers = []*registry.Header{ + { + Name: "Authorization", + Description: "Auth header", + Required: true, + Secret: true, + }, + } + + serverJSON, err := RemoteServerMetadataToServerJSON("test", remoteMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.Len(t, serverJSON.Remotes, 1) + require.Len(t, serverJSON.Remotes[0].Headers, 1) + + header := serverJSON.Remotes[0].Headers[0] + assert.Equal(t, "Authorization", header.Name) + assert.Equal(t, "Auth header", header.Description) + assert.True(t, header.IsRequired) + assert.True(t, header.IsSecret) +} + +func TestRemoteServerMetadataToServerJSON_WithPublisherExtensions(t *testing.T) { + t.Parallel() + + remoteMetadata := createTestRemoteServerMetadata() + serverJSON, err := RemoteServerMetadataToServerJSON("test", remoteMetadata) + + require.NoError(t, err) + require.NotNil(t, serverJSON) + require.NotNil(t, serverJSON.Meta) + require.NotNil(t, serverJSON.Meta.PublisherProvided) + + stacklokData, ok := serverJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + require.True(t, ok) + + remoteData, ok := stacklokData["https://api.example.com/mcp"].(map[string]interface{}) + require.True(t, ok) + + assert.Equal(t, "active", remoteData["status"]) + assert.Equal(t, "Official", remoteData["tier"]) +} + +// Test Suite 5: Utility Functions + +func TestExtractServerName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + { + name: "reverse DNS format", + input: "io.github.stacklok/fetch", + expected: "fetch", + }, + { + name: "no slash", + input: "fetch", + expected: "fetch", + }, + { + name: "returns original if multiple slashes", + input: "io.github.stacklok/mcp/server", + expected: "io.github.stacklok/mcp/server", // Function only splits on first slash, returns original if not exactly 2 parts + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ExtractServerName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBuildReverseDNSName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple name", + input: "fetch", + expected: "io.github.stacklok/fetch", + }, + { + name: "already formatted", + input: "io.github.stacklok/fetch", + expected: "io.github.stacklok/fetch", + }, + { + name: "other namespace", + input: "com.example/server", + expected: "com.example/server", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := BuildReverseDNSName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test Suite 6: Round-trip Conversion Tests + +func TestRoundTrip_ImageMetadata(t *testing.T) { + t.Parallel() + + // Start with ImageMetadata + original := createTestImageMetadata() + + // Convert to ServerJSON + serverJSON, err := ImageMetadataToServerJSON("test-server", original) + require.NoError(t, err) + + // Convert back to ImageMetadata + result, err := ServerJSONToImageMetadata(serverJSON) + require.NoError(t, err) + + // Verify data preserved + assert.Equal(t, original.Image, result.Image) + assert.Equal(t, original.Description, result.Description) + assert.Equal(t, original.Transport, result.Transport) + assert.Equal(t, original.RepositoryURL, result.RepositoryURL) + assert.Equal(t, original.Status, result.Status) + assert.Equal(t, original.Tier, result.Tier) + assert.Equal(t, original.Tools, result.Tools) + assert.Equal(t, original.Tags, result.Tags) + + if original.Metadata != nil { + require.NotNil(t, result.Metadata) + assert.Equal(t, original.Metadata.Stars, result.Metadata.Stars) + assert.Equal(t, original.Metadata.Pulls, result.Metadata.Pulls) + assert.Equal(t, original.Metadata.LastUpdated, result.Metadata.LastUpdated) + } +} + +func TestRoundTrip_RemoteServerMetadata(t *testing.T) { + t.Parallel() + + // Start with RemoteServerMetadata + original := createTestRemoteServerMetadata() + + // Convert to ServerJSON + serverJSON, err := RemoteServerMetadataToServerJSON("test-remote", original) + require.NoError(t, err) + + // Convert back to RemoteServerMetadata + result, err := ServerJSONToRemoteServerMetadata(serverJSON) + require.NoError(t, err) + + // Verify data preserved + assert.Equal(t, original.URL, result.URL) + assert.Equal(t, original.Description, result.Description) + assert.Equal(t, original.Transport, result.Transport) + assert.Equal(t, original.RepositoryURL, result.RepositoryURL) + assert.Equal(t, original.Status, result.Status) + assert.Equal(t, original.Tier, result.Tier) + assert.Equal(t, original.Tools, result.Tools) + assert.Equal(t, original.Tags, result.Tags) +} + +func TestRoundTrip_ImageMetadataWithAllFields(t *testing.T) { + t.Parallel() + + // Create ImageMetadata with maximum field population + original := ®istry.ImageMetadata{ + BaseServerMetadata: registry.BaseServerMetadata{ + Description: "Full featured server", + Transport: model.TransportTypeStreamableHTTP, + RepositoryURL: "https://github.com/test/full", + Status: "active", + Tier: "Official", + Tools: []string{"tool1", "tool2", "tool3"}, + Tags: []string{"tag1", "tag2"}, + Metadata: ®istry.Metadata{ + Stars: 500, + Pulls: 10000, + LastUpdated: "2025-10-23", + }, + }, + Image: "ghcr.io/test/full:v1.0.0", + TargetPort: 8080, + EnvVars: []*registry.EnvVar{ + { + Name: "API_KEY", + Description: "API Key for authentication", + Required: true, + Secret: true, + Default: "", + }, + { + Name: "LOG_LEVEL", + Description: "Logging level", + Required: false, + Secret: false, + Default: "info", + }, + }, + } + + // Round trip + serverJSON, err := ImageMetadataToServerJSON("full-server", original) + require.NoError(t, err) + + result, err := ServerJSONToImageMetadata(serverJSON) + require.NoError(t, err) + + // Verify all fields preserved + assert.Equal(t, original.Image, result.Image) + assert.Equal(t, original.Description, result.Description) + assert.Equal(t, original.Transport, result.Transport) + assert.Equal(t, original.RepositoryURL, result.RepositoryURL) + assert.Equal(t, original.Status, result.Status) + assert.Equal(t, original.Tier, result.Tier) + assert.Equal(t, original.Tools, result.Tools) + assert.Equal(t, original.Tags, result.Tags) + assert.Equal(t, original.TargetPort, result.TargetPort) + + require.Len(t, result.EnvVars, len(original.EnvVars)) + for i := range original.EnvVars { + assert.Equal(t, original.EnvVars[i].Name, result.EnvVars[i].Name) + assert.Equal(t, original.EnvVars[i].Description, result.EnvVars[i].Description) + assert.Equal(t, original.EnvVars[i].Required, result.EnvVars[i].Required) + assert.Equal(t, original.EnvVars[i].Secret, result.EnvVars[i].Secret) + assert.Equal(t, original.EnvVars[i].Default, result.EnvVars[i].Default) + } + + require.NotNil(t, result.Metadata) + assert.Equal(t, original.Metadata.Stars, result.Metadata.Stars) + assert.Equal(t, original.Metadata.Pulls, result.Metadata.Pulls) + assert.Equal(t, original.Metadata.LastUpdated, result.Metadata.LastUpdated) +} + +// TestRealWorld_GitHubServer tests conversion using the actual GitHub MCP server data as a template +// This test verifies that our converters can handle real-world production data correctly +func TestRealWorld_GitHubServer(t *testing.T) { + t.Parallel() + + // Create the official ServerJSON format (from the actual registry) + officialFormat := &upstream.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "io.github.github/github-mcp-server", + Description: "Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.", + Version: "0.19.1", + Repository: &model.Repository{ + URL: "https://github.com/github/github-mcp-server", + Source: "github", + }, + Packages: []model.Package{ + { + RegistryType: model.RegistryTypeOCI, + Identifier: "ghcr.io/github/github-mcp-server:0.19.1", + Transport: model.Transport{ + Type: model.TransportTypeStdio, + }, + EnvironmentVariables: []model.KeyValueInput{ + { + Name: "GITHUB_PERSONAL_ACCESS_TOKEN", + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "Your GitHub personal access token with appropriate scopes.", + IsRequired: true, + IsSecret: true, + }, + }, + }, + }, + }, + }, + Meta: &upstream.ServerMeta{ + PublisherProvided: map[string]interface{}{ + "io.github.stacklok": map[string]interface{}{ + "ghcr.io/github/github-mcp-server:0.19.1": map[string]interface{}{ + "status": "active", + "tier": "Official", + "tools": []interface{}{ + "add_comment_to_pending_review", "add_issue_comment", "add_sub_issue", + "assign_copilot_to_issue", "create_branch", "create_issue", + "create_or_update_file", "create_pull_request", "create_repository", + "delete_file", "fork_repository", "get_commit", "get_file_contents", + "get_issue", "get_issue_comments", "get_label", "get_latest_release", + "get_me", "get_release_by_tag", "get_tag", "get_team_members", + "get_teams", "list_branches", "list_commits", "list_issue_types", + "list_issues", "list_label", "list_pull_requests", "list_releases", + "list_sub_issues", "list_tags", "merge_pull_request", + "pull_request_read", "pull_request_review_write", "push_files", + "remove_sub_issue", "reprioritize_sub_issue", "request_copilot_review", + "search_code", "search_issues", "search_pull_requests", + "search_repositories", "search_users", "update_issue", + "update_pull_request", "update_pull_request_branch", + }, + "tags": []interface{}{ + "api", "create", "fork", "github", "list", + "pull-request", "push", "repository", "search", "update", "issues", + }, + "metadata": map[string]interface{}{ + "stars": float64(23700), + "pulls": float64(5000), + "last_updated": "2025-10-18T02:26:51Z", + }, + }, + }, + }, + }, + } + + // Convert official format to ImageMetadata + imageMetadata, err := ServerJSONToImageMetadata(officialFormat) + require.NoError(t, err, "Should convert official format to ImageMetadata") + require.NotNil(t, imageMetadata) + + // Verify core fields + assert.Equal(t, "Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.", imageMetadata.Description) + assert.Equal(t, "stdio", imageMetadata.Transport) + assert.Equal(t, "ghcr.io/github/github-mcp-server:0.19.1", imageMetadata.Image) + assert.Equal(t, "https://github.com/github/github-mcp-server", imageMetadata.RepositoryURL) + + // Verify environment variables + require.Len(t, imageMetadata.EnvVars, 1) + assert.Equal(t, "GITHUB_PERSONAL_ACCESS_TOKEN", imageMetadata.EnvVars[0].Name) + assert.Equal(t, "Your GitHub personal access token with appropriate scopes.", imageMetadata.EnvVars[0].Description) + assert.True(t, imageMetadata.EnvVars[0].Required) + assert.True(t, imageMetadata.EnvVars[0].Secret) + + // Verify publisher extensions were extracted + assert.Equal(t, "active", imageMetadata.Status) + assert.Equal(t, "Official", imageMetadata.Tier) + require.Len(t, imageMetadata.Tools, 46, "Should have 46 tools") + assert.Contains(t, imageMetadata.Tools, "create_pull_request") + assert.Contains(t, imageMetadata.Tools, "search_repositories") + require.Len(t, imageMetadata.Tags, 11, "Should have 11 tags") + assert.Contains(t, imageMetadata.Tags, "github") + assert.Contains(t, imageMetadata.Tags, "pull-request") + + // Verify metadata + require.NotNil(t, imageMetadata.Metadata) + assert.Equal(t, 23700, imageMetadata.Metadata.Stars) + assert.Equal(t, 5000, imageMetadata.Metadata.Pulls) + assert.Equal(t, "2025-10-18T02:26:51Z", imageMetadata.Metadata.LastUpdated) + + // Test round-trip: Convert back to ServerJSON + resultServerJSON, err := ImageMetadataToServerJSON("github-mcp-server", imageMetadata) + require.NoError(t, err, "Should convert ImageMetadata back to ServerJSON") + require.NotNil(t, resultServerJSON) + + // Verify round-trip preserved core data + assert.Equal(t, "io.github.stacklok/github-mcp-server", resultServerJSON.Name) + assert.Equal(t, officialFormat.Description, resultServerJSON.Description) + assert.Equal(t, officialFormat.Repository.URL, resultServerJSON.Repository.URL) + + // Verify packages + require.Len(t, resultServerJSON.Packages, 1) + assert.Equal(t, model.RegistryTypeOCI, resultServerJSON.Packages[0].RegistryType) + assert.Equal(t, "ghcr.io/github/github-mcp-server:0.19.1", resultServerJSON.Packages[0].Identifier) + assert.Equal(t, model.TransportTypeStdio, resultServerJSON.Packages[0].Transport.Type) + + // Verify publisher extensions are present in round-trip + require.NotNil(t, resultServerJSON.Meta) + require.NotNil(t, resultServerJSON.Meta.PublisherProvided) + stacklokData, ok := resultServerJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + require.True(t, ok, "Should have io.github.stacklok namespace") + imageData, ok := stacklokData["ghcr.io/github/github-mcp-server:0.19.1"].(map[string]interface{}) + require.True(t, ok, "Should have image-specific extensions") + + // Verify extensions preserved + assert.Equal(t, "active", imageData["status"]) + assert.Equal(t, "Official", imageData["tier"]) + + // Verify tools are preserved as interface slice + tools, ok := imageData["tools"].([]interface{}) + require.True(t, ok, "Tools should be []interface{}") + assert.Len(t, tools, 46) + + // Verify tags are preserved + tags, ok := imageData["tags"].([]interface{}) + require.True(t, ok, "Tags should be []interface{}") + assert.Len(t, tags, 11) + + // Verify metadata is preserved + metadata, ok := imageData["metadata"].(map[string]interface{}) + require.True(t, ok, "Metadata should be present") + assert.Equal(t, float64(23700), metadata["stars"]) + assert.Equal(t, float64(5000), metadata["pulls"]) + assert.Equal(t, "2025-10-18T02:26:51Z", metadata["last_updated"]) +} + +// TestRealWorld_GitHubServer_ExactData tests conversion using EXACT data from the user +// This uses the actual JSON strings provided to verify visual correctness +func TestRealWorld_GitHubServer_ExactData(t *testing.T) { + t.Parallel() + + // EXACT ImageMetadata format as provided by user (from build/registry.json) + imageMetadataJSON := `{ + "description": "Provides integration with GitHub's APIs", + "tier": "Official", + "status": "Active", + "transport": "stdio", + "tools": [ + "add_comment_to_pending_review", + "add_issue_comment", + "add_sub_issue", + "assign_copilot_to_issue", + "create_branch", + "create_issue", + "create_or_update_file", + "create_pull_request", + "create_repository", + "delete_file", + "fork_repository", + "get_commit", + "get_file_contents", + "get_issue", + "get_issue_comments", + "get_label", + "get_latest_release", + "get_me", + "get_release_by_tag", + "get_tag", + "get_team_members", + "get_teams", + "list_branches", + "list_commits", + "list_issue_types", + "list_issues", + "list_label", + "list_pull_requests", + "list_releases", + "list_sub_issues", + "list_tags", + "merge_pull_request", + "pull_request_read", + "pull_request_review_write", + "push_files", + "remove_sub_issue", + "reprioritize_sub_issue", + "request_copilot_review", + "search_code", + "search_issues", + "search_pull_requests", + "search_repositories", + "search_users", + "update_issue", + "update_pull_request", + "update_pull_request_branch" + ], + "metadata": { + "stars": 23700, + "pulls": 5000, + "last_updated": "2025-10-18T02:26:51Z" + }, + "repository_url": "https://github.com/github/github-mcp-server", + "tags": [ + "api", + "create", + "fork", + "github", + "list", + "pull-request", + "push", + "repository", + "search", + "update", + "issues" + ], + "image": "ghcr.io/github/github-mcp-server:v0.19.1", + "permissions": { + "network": { + "outbound": { + "allow_host": [ + ".github.com", + ".githubusercontent.com" + ], + "allow_port": [ + 443 + ] + } + } + }, + "env_vars": [ + { + "name": "GITHUB_PERSONAL_ACCESS_TOKEN", + "description": "GitHub personal access token with appropriate permissions", + "required": true, + "secret": true + }, + { + "name": "GITHUB_HOST", + "description": "GitHub Enterprise Server hostname (optional)", + "required": false + }, + { + "name": "GITHUB_TOOLSETS", + "description": "Comma-separated list of toolsets to enable (e.g., 'repos,issues,pull_requests'). If not set, all toolsets are enabled. See the README for available toolsets.", + "required": false + }, + { + "name": "GITHUB_DYNAMIC_TOOLSETS", + "description": "Set to '1' to enable dynamic toolset discovery", + "required": false + }, + { + "name": "GITHUB_READ_ONLY", + "description": "Set to '1' to enable read-only mode, preventing any modifications to GitHub resources", + "required": false + } + ], + "provenance": { + "sigstore_url": "tuf-repo-cdn.sigstore.dev", + "repository_uri": "https://github.com/github/github-mcp-server", + "signer_identity": "/.github/workflows/docker-publish.yml", + "runner_environment": "github-hosted", + "cert_issuer": "https://token.actions.githubusercontent.com" + } +}` + + // Parse ImageMetadata JSON + var imageMetadata registry.ImageMetadata + err := json.Unmarshal([]byte(imageMetadataJSON), &imageMetadata) + require.NoError(t, err, "Should parse ImageMetadata JSON") + + // Log the parsed structure for visual inspection + t.Logf("Parsed ImageMetadata:") + t.Logf(" Description: %s", imageMetadata.Description) + t.Logf(" Image: %s", imageMetadata.Image) + t.Logf(" Status: %s", imageMetadata.Status) + t.Logf(" Tier: %s", imageMetadata.Tier) + t.Logf(" Tools: %d items", len(imageMetadata.Tools)) + t.Logf(" EnvVars: %d items", len(imageMetadata.EnvVars)) + t.Logf(" Tags: %d items", len(imageMetadata.Tags)) + + // Verify parsed data matches expectations + assert.Equal(t, "Provides integration with GitHub's APIs", imageMetadata.Description) + assert.Equal(t, "ghcr.io/github/github-mcp-server:v0.19.1", imageMetadata.Image) + assert.Equal(t, "Active", imageMetadata.Status) + assert.Equal(t, "Official", imageMetadata.Tier) + assert.Equal(t, "stdio", imageMetadata.Transport) + assert.Len(t, imageMetadata.Tools, 46) + assert.Len(t, imageMetadata.EnvVars, 5) + assert.Len(t, imageMetadata.Tags, 11) + assert.NotNil(t, imageMetadata.Permissions) + assert.NotNil(t, imageMetadata.Provenance) + + // Convert to official ServerJSON format + serverJSON, err := ImageMetadataToServerJSON("github", &imageMetadata) + require.NoError(t, err, "Should convert ImageMetadata to ServerJSON") + require.NotNil(t, serverJSON) + + // Marshal to JSON for visual inspection + serverJSONBytes, err := json.MarshalIndent(serverJSON, "", " ") + require.NoError(t, err) + t.Logf("\n\nConverted to Official ServerJSON:\n%s", string(serverJSONBytes)) + + // Verify official format structure + assert.Equal(t, model.CurrentSchemaURL, serverJSON.Schema) + assert.Equal(t, "io.github.stacklok/github", serverJSON.Name) + assert.Equal(t, "Provides integration with GitHub's APIs", serverJSON.Description) + require.Len(t, serverJSON.Packages, 1) + assert.Equal(t, "ghcr.io/github/github-mcp-server:v0.19.1", serverJSON.Packages[0].Identifier) + assert.Len(t, serverJSON.Packages[0].EnvironmentVariables, 5) + + // Verify publisher extensions contain all the extra data + require.NotNil(t, serverJSON.Meta) + require.NotNil(t, serverJSON.Meta.PublisherProvided) + stacklokData, ok := serverJSON.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{}) + require.True(t, ok) + extensions, ok := stacklokData["ghcr.io/github/github-mcp-server:v0.19.1"].(map[string]interface{}) + require.True(t, ok) + + // Verify extensions + assert.Equal(t, "Active", extensions["status"]) + assert.Equal(t, "Official", extensions["tier"]) + assert.NotNil(t, extensions["tools"]) + assert.NotNil(t, extensions["tags"]) + assert.NotNil(t, extensions["metadata"]) + // NOTE: Permissions and provenance would need to be added to the converter functions + // Uncomment once converters.go is updated to include them in publisher extensions: + // assert.NotNil(t, extensions["permissions"]) + // assert.NotNil(t, extensions["provenance"]) + + // Test round-trip: Convert back to ImageMetadata + roundTripImageMetadata, err := ServerJSONToImageMetadata(serverJSON) + require.NoError(t, err, "Should convert ServerJSON back to ImageMetadata") + require.NotNil(t, roundTripImageMetadata) + + // Marshal round-trip result for visual inspection + roundTripBytes, err := json.MarshalIndent(roundTripImageMetadata, "", " ") + require.NoError(t, err) + t.Logf("\n\nRound-trip back to ImageMetadata:\n%s", string(roundTripBytes)) + + // Verify round-trip preserved all data + assert.Equal(t, imageMetadata.Description, roundTripImageMetadata.Description) + assert.Equal(t, imageMetadata.Image, roundTripImageMetadata.Image) + assert.Equal(t, imageMetadata.Status, roundTripImageMetadata.Status) + assert.Equal(t, imageMetadata.Tier, roundTripImageMetadata.Tier) + assert.Equal(t, imageMetadata.Transport, roundTripImageMetadata.Transport) + assert.Equal(t, imageMetadata.RepositoryURL, roundTripImageMetadata.RepositoryURL) + assert.Equal(t, imageMetadata.Tools, roundTripImageMetadata.Tools) + assert.Equal(t, imageMetadata.Tags, roundTripImageMetadata.Tags) + assert.Len(t, roundTripImageMetadata.EnvVars, 5) + + // Verify all 5 env vars + envVarNames := []string{} + for _, ev := range roundTripImageMetadata.EnvVars { + envVarNames = append(envVarNames, ev.Name) + } + assert.Contains(t, envVarNames, "GITHUB_PERSONAL_ACCESS_TOKEN") + assert.Contains(t, envVarNames, "GITHUB_HOST") + assert.Contains(t, envVarNames, "GITHUB_TOOLSETS") + assert.Contains(t, envVarNames, "GITHUB_DYNAMIC_TOOLSETS") + assert.Contains(t, envVarNames, "GITHUB_READ_ONLY") + + // Verify metadata preserved + require.NotNil(t, roundTripImageMetadata.Metadata) + assert.Equal(t, 23700, roundTripImageMetadata.Metadata.Stars) + assert.Equal(t, 5000, roundTripImageMetadata.Metadata.Pulls) + + // NOTE: Permissions and provenance are currently stored in publisher extensions + // but not extracted back during conversion. This is expected behavior for now. + // They are preserved in the ServerJSON._meta but would need extraction logic + // added to converters.go:extractImageExtensions() to be round-tripped. + // + // Uncomment these assertions once extraction logic is added: + // assert.NotNil(t, roundTripImageMetadata.Permissions) + // assert.NotNil(t, roundTripImageMetadata.Provenance) +} diff --git a/pkg/registry/converters/integration_test.go b/pkg/registry/converters/integration_test.go new file mode 100644 index 000000000..02f19f7e4 --- /dev/null +++ b/pkg/registry/converters/integration_test.go @@ -0,0 +1,412 @@ +package converters + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/stacklok/toolhive/pkg/registry" + "github.com/stretchr/testify/require" +) + +// ToolHiveRegistry represents the structure of registry.json +type ToolHiveRegistry struct { + Servers map[string]json.RawMessage `json:"servers"` + RemoteServers map[string]json.RawMessage `json:"remote_servers"` +} + +// parseServerEntry parses a server entry as either ImageMetadata or RemoteServerMetadata +func parseServerEntry(data json.RawMessage) (imageMetadata *registry.ImageMetadata, remoteMetadata *registry.RemoteServerMetadata, err error) { + // Try to detect type by checking for "image" vs "url" field + var typeCheck map[string]interface{} + if err := json.Unmarshal(data, &typeCheck); err != nil { + return nil, nil, err + } + + if _, hasImage := typeCheck["image"]; hasImage { + // It's an ImageMetadata + var img registry.ImageMetadata + if err := json.Unmarshal(data, &img); err != nil { + return nil, nil, err + } + return &img, nil, nil + } else if _, hasURL := typeCheck["url"]; hasURL { + // It's a RemoteServerMetadata + var remote registry.RemoteServerMetadata + if err := json.Unmarshal(data, &remote); err != nil { + return nil, nil, err + } + return nil, &remote, nil + } + + return nil, nil, fmt.Errorf("unable to determine server type") +} + +// OfficialRegistry represents the structure of official-registry.json +type OfficialRegistry struct { + Data struct { + Servers []upstream.ServerJSON `json:"servers"` + } `json:"data"` +} + +// TestRoundTrip_RealRegistryData tests that we can convert the official registry back to toolhive format +// and that it matches the original registry.json +// Note: This is an integration test that reads from build/ directory, so we don't run it in parallel +// +//nolint:paralleltest // Integration test reads from shared build/ directory +func TestRoundTrip_RealRegistryData(t *testing.T) { + // Skip if running in CI or if files don't exist + officialPath := filepath.Join("..", "..", "..", "build", "official-registry.json") + toolhivePath := filepath.Join("..", "..", "..", "build", "registry.json") + + if _, err := os.Stat(officialPath); os.IsNotExist(err) { + t.Skip("Skipping integration test: official-registry.json not found") + } + if _, err := os.Stat(toolhivePath); os.IsNotExist(err) { + t.Skip("Skipping integration test: registry.json not found") + } + + // Read official registry + officialData, err := os.ReadFile(officialPath) + require.NoError(t, err, "Failed to read official-registry.json") + + var officialRegistry OfficialRegistry + err = json.Unmarshal(officialData, &officialRegistry) + require.NoError(t, err, "Failed to parse official-registry.json") + + // Read toolhive registry + toolhiveData, err := os.ReadFile(toolhivePath) + require.NoError(t, err, "Failed to read registry.json") + + var toolhiveRegistry ToolHiveRegistry + err = json.Unmarshal(toolhiveData, &toolhiveRegistry) + require.NoError(t, err, "Failed to parse registry.json") + + t.Logf("Loaded %d servers from official registry", len(officialRegistry.Data.Servers)) + t.Logf("Loaded %d image servers and %d remote servers from toolhive registry", + len(toolhiveRegistry.Servers), len(toolhiveRegistry.RemoteServers)) + + // Track statistics + stats := struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string + }{} + + // For each server in official registry, convert back and compare + for _, serverJSON := range officialRegistry.Data.Servers { + stats.total++ + + // Extract simple name from reverse DNS format + simpleName := ExtractServerName(serverJSON.Name) + + t.Run(simpleName, func(t *testing.T) { + // Find corresponding entry in toolhive registry (check both servers and remote_servers) + var originalData json.RawMessage + var exists bool + + // Try servers first + originalData, exists = toolhiveRegistry.Servers[simpleName] + if !exists { + // Try remote_servers + originalData, exists = toolhiveRegistry.RemoteServers[simpleName] + if !exists { + t.Logf("⚠️ Server '%s' not found in toolhive registry (checked both servers and remote_servers)", simpleName) + return + } + } + + // Parse the original entry + originalImage, originalRemote, err := parseServerEntry(originalData) + if err != nil { + t.Errorf("❌ Failed to parse original entry: %v", err) + return + } + + // Determine if it's an image or remote server from official registry + isImage := len(serverJSON.Packages) > 0 + isRemote := len(serverJSON.Remotes) > 0 + + if isImage { + stats.imageServers++ + testImageServerRoundTrip(t, simpleName, &serverJSON, originalImage, &stats) + } else if isRemote { + stats.remoteServers++ + testRemoteServerRoundTrip(t, simpleName, &serverJSON, originalRemote, &stats) + } else { + t.Errorf("❌ Server '%s' has neither packages nor remotes", simpleName) + } + }) + } + + // Print summary + separator := strings.Repeat("=", 80) + t.Logf("\n%s", separator) + t.Logf("INTEGRATION TEST SUMMARY") + t.Logf("%s", separator) + t.Logf("Total servers: %d", stats.total) + t.Logf(" - Image servers: %d", stats.imageServers) + t.Logf(" - Remote servers: %d", stats.remoteServers) + t.Logf("Conversion errors: %d", stats.conversionErrors) + t.Logf("Field mismatches: %d", len(stats.mismatches)) + + if len(stats.mismatches) > 0 { + t.Logf("\nMismatched fields:") + for _, mismatch := range stats.mismatches { + t.Logf(" - %s", mismatch) + } + } + + if stats.conversionErrors == 0 && len(stats.mismatches) == 0 { + t.Logf("\n✅ All servers converted successfully with no mismatches!") + } + t.Logf("%s", separator) +} + +func testImageServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *registry.ImageMetadata, stats *struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string +}) { + t.Helper() + if original == nil { + t.Errorf("❌ Original ImageMetadata is nil for '%s'", name) + return + } + + // Convert ServerJSON back to ImageMetadata + converted, err := ServerJSONToImageMetadata(serverJSON) + if err != nil { + t.Errorf("❌ Conversion failed: %v", err) + stats.conversionErrors++ + return + } + + // Compare fields + compareImageMetadata(t, name, original, converted, stats) +} + +func testRemoteServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *registry.RemoteServerMetadata, stats *struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string +}) { + t.Helper() + if original == nil { + t.Errorf("❌ Original RemoteServerMetadata is nil for '%s'", name) + return + } + + // Convert ServerJSON back to RemoteServerMetadata + converted, err := ServerJSONToRemoteServerMetadata(serverJSON) + if err != nil { + t.Errorf("❌ Conversion failed: %v", err) + stats.conversionErrors++ + return + } + + // Compare fields + compareRemoteServerMetadata(t, name, original, converted, stats) +} + +func compareImageMetadata(t *testing.T, name string, original, converted *registry.ImageMetadata, stats *struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string +}) { + t.Helper() + // Compare basic fields + if original.Image != converted.Image { + recordMismatch(t, stats, name, "Image", original.Image, converted.Image) + } + if original.Description != converted.Description { + recordMismatch(t, stats, name, "Description", original.Description, converted.Description) + } + if original.Transport != converted.Transport { + recordMismatch(t, stats, name, "Transport", original.Transport, converted.Transport) + } + if original.RepositoryURL != converted.RepositoryURL { + recordMismatch(t, stats, name, "RepositoryURL", original.RepositoryURL, converted.RepositoryURL) + } + if original.Status != converted.Status { + recordMismatch(t, stats, name, "Status", original.Status, converted.Status) + } + if original.Tier != converted.Tier { + recordMismatch(t, stats, name, "Tier", original.Tier, converted.Tier) + } + if original.TargetPort != converted.TargetPort { + recordMismatch(t, stats, name, "TargetPort", original.TargetPort, converted.TargetPort) + } + + // Compare slices + if !stringSlicesEqual(original.Tools, converted.Tools) { + recordMismatch(t, stats, name, "Tools", original.Tools, converted.Tools) + } + if !stringSlicesEqual(original.Tags, converted.Tags) { + recordMismatch(t, stats, name, "Tags", original.Tags, converted.Tags) + } + + // Compare EnvVars + if len(original.EnvVars) != len(converted.EnvVars) { + recordMismatch(t, stats, name, "EnvVars.length", len(original.EnvVars), len(converted.EnvVars)) + } else { + for i := range original.EnvVars { + if !envVarsEqual(original.EnvVars[i], converted.EnvVars[i]) { + recordMismatch(t, stats, name, fmt.Sprintf("EnvVars[%d]", i), original.EnvVars[i], converted.EnvVars[i]) + } + } + } + + // Compare Metadata + if !metadataEqual(original.Metadata, converted.Metadata) { + recordMismatch(t, stats, name, "Metadata", original.Metadata, converted.Metadata) + } + + // Note: Permissions, Provenance, Args are in extensions and may not round-trip perfectly + // We'll log these separately if they differ + if original.Permissions != nil && converted.Permissions == nil { + t.Logf("⚠️ '%s': Permissions not preserved in round-trip", name) + } + if original.Provenance != nil && converted.Provenance == nil { + t.Logf("⚠️ '%s': Provenance not preserved in round-trip", name) + } + if len(original.Args) > 0 && len(converted.Args) == 0 { + t.Logf("⚠️ '%s': Args not preserved in round-trip", name) + } +} + +func compareRemoteServerMetadata(t *testing.T, name string, original, converted *registry.RemoteServerMetadata, stats *struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string +}) { + t.Helper() + // Compare basic fields + if original.URL != converted.URL { + recordMismatch(t, stats, name, "URL", original.URL, converted.URL) + } + if original.Description != converted.Description { + recordMismatch(t, stats, name, "Description", original.Description, converted.Description) + } + if original.Transport != converted.Transport { + recordMismatch(t, stats, name, "Transport", original.Transport, converted.Transport) + } + if original.RepositoryURL != converted.RepositoryURL { + recordMismatch(t, stats, name, "RepositoryURL", original.RepositoryURL, converted.RepositoryURL) + } + if original.Status != converted.Status { + recordMismatch(t, stats, name, "Status", original.Status, converted.Status) + } + if original.Tier != converted.Tier { + recordMismatch(t, stats, name, "Tier", original.Tier, converted.Tier) + } + + // Compare slices + if !stringSlicesEqual(original.Tools, converted.Tools) { + recordMismatch(t, stats, name, "Tools", original.Tools, converted.Tools) + } + if !stringSlicesEqual(original.Tags, converted.Tags) { + recordMismatch(t, stats, name, "Tags", original.Tags, converted.Tags) + } + + // Compare Headers + if len(original.Headers) != len(converted.Headers) { + recordMismatch(t, stats, name, "Headers.length", len(original.Headers), len(converted.Headers)) + } else { + for i := range original.Headers { + if !headersEqual(original.Headers[i], converted.Headers[i]) { + recordMismatch(t, stats, name, fmt.Sprintf("Headers[%d]", i), original.Headers[i], converted.Headers[i]) + } + } + } + + // Compare Metadata + if !metadataEqual(original.Metadata, converted.Metadata) { + recordMismatch(t, stats, name, "Metadata", original.Metadata, converted.Metadata) + } + + // Note: OAuthConfig may not round-trip perfectly + if original.OAuthConfig != nil && converted.OAuthConfig == nil { + t.Logf("⚠️ '%s': OAuthConfig not preserved in round-trip", name) + } +} + +func recordMismatch(t *testing.T, stats *struct { + total int + imageServers int + remoteServers int + conversionErrors int + mismatches []string +}, serverName, field string, original, converted interface{}) { + t.Helper() + msg := fmt.Sprintf("%s.%s: expected %v, got %v", serverName, field, original, converted) + stats.mismatches = append(stats.mismatches, msg) + t.Logf("⚠️ %s", msg) +} + +// Helper comparison functions + +func stringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func envVarsEqual(a, b *registry.EnvVar) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Name == b.Name && + a.Description == b.Description && + a.Required == b.Required && + a.Secret == b.Secret && + a.Default == b.Default +} + +func headersEqual(a, b *registry.Header) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Name == b.Name && + a.Description == b.Description && + a.Required == b.Required && + a.Secret == b.Secret +} + +func metadataEqual(a, b *registry.Metadata) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Stars == b.Stars && + a.Pulls == b.Pulls && + a.LastUpdated == b.LastUpdated +} diff --git a/pkg/registry/converters/testdata/README.md b/pkg/registry/converters/testdata/README.md new file mode 100644 index 000000000..fb711c8b4 --- /dev/null +++ b/pkg/registry/converters/testdata/README.md @@ -0,0 +1,174 @@ +# Converter Test Fixtures + +This directory contains JSON fixture files for testing the converter functions between ToolHive ImageMetadata/RemoteServerMetadata and official MCP ServerJSON formats. + +## Directory Structure + +``` +testdata/ +├── image_to_server/ # ImageMetadata → ServerJSON conversions +├── server_to_image/ # ServerJSON → ImageMetadata conversions +├── remote_to_server/ # RemoteServerMetadata → ServerJSON conversions +└── server_to_remote/ # ServerJSON → RemoteServerMetadata conversions +``` + +Each directory contains: +- `input_*.json` - Input data for the conversion +- `expected_*.json` - Expected output after conversion + +## Test Coverage + +### Image-based Servers + +**GitHub Server** (`image_to_server/input_github.json`) +- Full production example with 46 tools +- 5 environment variables (including optional ones) +- Permissions and provenance metadata +- Demonstrates complete ToolHive → Official MCP conversion + +**Round-trip** (`server_to_image/`) +- Uses the output from `image_to_server` as input +- Validates bidirectional conversion without data loss +- Ensures all fields are preserved through the conversion cycle + +### Remote Servers + +**Example Remote** (`remote_to_server/input_example.json`) +- SSE transport type +- Multiple headers (required and optional) +- Demonstrates remote server conversion pattern + +**Round-trip** (`server_to_remote/`) +- Validates remote server bidirectional conversion +- Ensures headers and metadata are preserved + +## Usage in Tests + +The fixtures are used by `converters_fixture_test.go`: + +```go +func TestConverters_Fixtures(t *testing.T) { + // Table-driven test that: + // 1. Loads input fixture + // 2. Runs conversion + // 3. Compares with expected output + // 4. Runs additional validation checks +} +``` + +## Adding New Test Cases + +To add a new test case: + +1. **Create input file** in the appropriate directory: + ```bash + # For image-based server + vi testdata/image_to_server/input_myserver.json + ``` + +2. **Generate expected output** using the converter: + ```go + // Example code to generate expected output + imageMetadata := loadFromFile("input_myserver.json") + serverJSON, _ := ImageMetadataToServerJSON("myserver", imageMetadata) + saveToFile("expected_myserver.json", serverJSON) + ``` + +3. **Add test case** to `converters_fixture_test.go`: + ```go + { + name: "ImageMetadata to ServerJSON - MyServer", + fixtureDir: "testdata/image_to_server", + inputFile: "input_myserver.json", + expectedFile: "expected_myserver.json", + serverName: "myserver", + convertFunc: "ImageToServer", + validateFunc: validateImageToServerConversion, + }, + ``` + +## Fixture Format Examples + +### ImageMetadata Format +```json +{ + "description": "Server description", + "tier": "Official", + "status": "Active", + "transport": "stdio", + "tools": ["tool1", "tool2"], + "image": "ghcr.io/org/server:v1.0.0", + "env_vars": [...], + "permissions": {...}, + "provenance": {...} +} +``` + +### ServerJSON Format +```json +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.stacklok/server", + "description": "Server description", + "version": "1.0.0", + "packages": [{ + "registryType": "oci", + "identifier": "ghcr.io/org/server:v1.0.0", + "transport": {"type": "stdio"}, + "environmentVariables": [...] + }], + "_meta": { + "io.modelcontextprotocol.registry/publisher-provided": { + "io.github.stacklok": { + "ghcr.io/org/server:v1.0.0": { + "status": "Active", + "tier": "Official", + "tools": [...], + ... + } + } + } + } +} +``` + +### RemoteServerMetadata Format +```json +{ + "description": "Remote server description", + "transport": "sse", + "url": "https://api.example.com/mcp", + "headers": [ + { + "name": "Authorization", + "description": "Auth header", + "required": true, + "secret": true + } + ], + "tools": [...], + "tags": [...] +} +``` + +## Benefits of Fixture-based Testing + +1. **Visual Inspection** - Easy to see exactly what data is being transformed +2. **Maintainability** - Update fixtures without changing test code +3. **Documentation** - Fixtures serve as examples of expected formats +4. **Regression Detection** - Any changes to output format are immediately visible +5. **Multiple Scenarios** - Easy to add edge cases and variants + +## Regenerating Fixtures + +If the converter logic changes and you need to regenerate expected outputs: + +```bash +# Regenerate all expected outputs +go run scripts/regenerate_fixtures.go + +# Or regenerate specific ones +go run scripts/regenerate_fixtures.go --type image_to_server --name github +``` + +(Note: Create the regenerate script if needed) \ No newline at end of file diff --git a/pkg/registry/converters/testdata/image_to_server/expected_github.json b/pkg/registry/converters/testdata/image_to_server/expected_github.json new file mode 100644 index 000000000..f86593b81 --- /dev/null +++ b/pkg/registry/converters/testdata/image_to_server/expected_github.json @@ -0,0 +1,139 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.stacklok/github", + "description": "Provides integration with GitHub's APIs", + "repository": { + "url": "https://github.com/github/github-mcp-server", + "source": "github" + }, + "version": "1.0.0", + "packages": [ + { + "registryType": "oci", + "identifier": "ghcr.io/github/github-mcp-server:v0.19.1", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "description": "GitHub personal access token with appropriate permissions", + "isRequired": true, + "isSecret": true, + "name": "GITHUB_PERSONAL_ACCESS_TOKEN" + }, + { + "description": "GitHub Enterprise Server hostname (optional)", + "name": "GITHUB_HOST" + }, + { + "description": "Comma-separated list of toolsets to enable (e.g., 'repos,issues,pull_requests'). If not set, all toolsets are enabled. See the README for available toolsets.", + "name": "GITHUB_TOOLSETS" + }, + { + "description": "Set to '1' to enable dynamic toolset discovery", + "name": "GITHUB_DYNAMIC_TOOLSETS" + }, + { + "description": "Set to '1' to enable read-only mode, preventing any modifications to GitHub resources", + "name": "GITHUB_READ_ONLY" + } + ] + } + ], + "_meta": { + "io.modelcontextprotocol.registry/publisher-provided": { + "io.github.stacklok": { + "ghcr.io/github/github-mcp-server:v0.19.1": { + "metadata": { + "last_updated": "2025-10-18T02:26:51Z", + "pulls": 5000, + "stars": 23700 + }, + "permissions": { + "network": { + "outbound": { + "allow_host": [ + ".github.com", + ".githubusercontent.com" + ], + "allow_port": [ + 443 + ] + } + } + }, + "provenance": { + "cert_issuer": "https://token.actions.githubusercontent.com", + "repository_uri": "https://github.com/github/github-mcp-server", + "runner_environment": "github-hosted", + "signer_identity": "/.github/workflows/docker-publish.yml", + "sigstore_url": "tuf-repo-cdn.sigstore.dev" + }, + "status": "Active", + "tags": [ + "api", + "create", + "fork", + "github", + "list", + "pull-request", + "push", + "repository", + "search", + "update", + "issues" + ], + "tier": "Official", + "tools": [ + "add_comment_to_pending_review", + "add_issue_comment", + "add_sub_issue", + "assign_copilot_to_issue", + "create_branch", + "create_issue", + "create_or_update_file", + "create_pull_request", + "create_repository", + "delete_file", + "fork_repository", + "get_commit", + "get_file_contents", + "get_issue", + "get_issue_comments", + "get_label", + "get_latest_release", + "get_me", + "get_release_by_tag", + "get_tag", + "get_team_members", + "get_teams", + "list_branches", + "list_commits", + "list_issue_types", + "list_issues", + "list_label", + "list_pull_requests", + "list_releases", + "list_sub_issues", + "list_tags", + "merge_pull_request", + "pull_request_read", + "pull_request_review_write", + "push_files", + "remove_sub_issue", + "reprioritize_sub_issue", + "request_copilot_review", + "search_code", + "search_issues", + "search_pull_requests", + "search_repositories", + "search_users", + "update_issue", + "update_pull_request", + "update_pull_request_branch" + ] + } + } + } + } +} diff --git a/pkg/registry/converters/testdata/image_to_server/input_github.json b/pkg/registry/converters/testdata/image_to_server/input_github.json new file mode 100644 index 000000000..718efe459 --- /dev/null +++ b/pkg/registry/converters/testdata/image_to_server/input_github.json @@ -0,0 +1,122 @@ +{ + "description": "Provides integration with GitHub's APIs", + "tier": "Official", + "status": "Active", + "transport": "stdio", + "tools": [ + "add_comment_to_pending_review", + "add_issue_comment", + "add_sub_issue", + "assign_copilot_to_issue", + "create_branch", + "create_issue", + "create_or_update_file", + "create_pull_request", + "create_repository", + "delete_file", + "fork_repository", + "get_commit", + "get_file_contents", + "get_issue", + "get_issue_comments", + "get_label", + "get_latest_release", + "get_me", + "get_release_by_tag", + "get_tag", + "get_team_members", + "get_teams", + "list_branches", + "list_commits", + "list_issue_types", + "list_issues", + "list_label", + "list_pull_requests", + "list_releases", + "list_sub_issues", + "list_tags", + "merge_pull_request", + "pull_request_read", + "pull_request_review_write", + "push_files", + "remove_sub_issue", + "reprioritize_sub_issue", + "request_copilot_review", + "search_code", + "search_issues", + "search_pull_requests", + "search_repositories", + "search_users", + "update_issue", + "update_pull_request", + "update_pull_request_branch" + ], + "metadata": { + "stars": 23700, + "pulls": 5000, + "last_updated": "2025-10-18T02:26:51Z" + }, + "repository_url": "https://github.com/github/github-mcp-server", + "tags": [ + "api", + "create", + "fork", + "github", + "list", + "pull-request", + "push", + "repository", + "search", + "update", + "issues" + ], + "image": "ghcr.io/github/github-mcp-server:v0.19.1", + "permissions": { + "network": { + "outbound": { + "allow_host": [ + ".github.com", + ".githubusercontent.com" + ], + "allow_port": [ + 443 + ] + } + } + }, + "env_vars": [ + { + "name": "GITHUB_PERSONAL_ACCESS_TOKEN", + "description": "GitHub personal access token with appropriate permissions", + "required": true, + "secret": true + }, + { + "name": "GITHUB_HOST", + "description": "GitHub Enterprise Server hostname (optional)", + "required": false + }, + { + "name": "GITHUB_TOOLSETS", + "description": "Comma-separated list of toolsets to enable (e.g., 'repos,issues,pull_requests'). If not set, all toolsets are enabled. See the README for available toolsets.", + "required": false + }, + { + "name": "GITHUB_DYNAMIC_TOOLSETS", + "description": "Set to '1' to enable dynamic toolset discovery", + "required": false + }, + { + "name": "GITHUB_READ_ONLY", + "description": "Set to '1' to enable read-only mode, preventing any modifications to GitHub resources", + "required": false + } + ], + "provenance": { + "sigstore_url": "tuf-repo-cdn.sigstore.dev", + "repository_uri": "https://github.com/github/github-mcp-server", + "signer_identity": "/.github/workflows/docker-publish.yml", + "runner_environment": "github-hosted", + "cert_issuer": "https://token.actions.githubusercontent.com" + } +} diff --git a/pkg/registry/converters/testdata/remote_to_server/expected_example.json b/pkg/registry/converters/testdata/remote_to_server/expected_example.json new file mode 100644 index 000000000..9c40f736d --- /dev/null +++ b/pkg/registry/converters/testdata/remote_to_server/expected_example.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.stacklok/example-remote", + "description": "Example remote MCP server accessed via SSE", + "repository": { + "url": "https://github.com/example/remote-mcp-server", + "source": "github" + }, + "version": "1.0.0", + "remotes": [ + { + "type": "sse", + "url": "https://api.example.com/mcp", + "headers": [ + { + "description": "Bearer token for API authentication", + "isRequired": true, + "isSecret": true, + "name": "Authorization" + }, + { + "description": "API version to use", + "name": "X-API-Version" + } + ] + } + ], + "_meta": { + "io.modelcontextprotocol.registry/publisher-provided": { + "io.github.stacklok": { + "https://api.example.com/mcp": { + "metadata": { + "last_updated": "2025-10-20T10:00:00Z", + "pulls": 500, + "stars": 150 + }, + "status": "active", + "tags": [ + "remote", + "sse", + "api" + ], + "tier": "Community", + "tools": [ + "get_data", + "send_notification", + "query_api" + ] + } + } + } + } +} diff --git a/pkg/registry/converters/testdata/remote_to_server/input_example.json b/pkg/registry/converters/testdata/remote_to_server/input_example.json new file mode 100644 index 000000000..24b06f5ff --- /dev/null +++ b/pkg/registry/converters/testdata/remote_to_server/input_example.json @@ -0,0 +1,36 @@ +{ + "description": "Example remote MCP server accessed via SSE", + "tier": "Community", + "status": "active", + "transport": "sse", + "tools": [ + "get_data", + "send_notification", + "query_api" + ], + "metadata": { + "stars": 150, + "pulls": 500, + "last_updated": "2025-10-20T10:00:00Z" + }, + "repository_url": "https://github.com/example/remote-mcp-server", + "tags": [ + "remote", + "sse", + "api" + ], + "url": "https://api.example.com/mcp", + "headers": [ + { + "name": "Authorization", + "description": "Bearer token for API authentication", + "required": true, + "secret": true + }, + { + "name": "X-API-Version", + "description": "API version to use", + "required": false + } + ] +} diff --git a/pkg/registry/converters/testdata/server_to_image/expected_github.json b/pkg/registry/converters/testdata/server_to_image/expected_github.json new file mode 100644 index 000000000..875d3ecbc --- /dev/null +++ b/pkg/registry/converters/testdata/server_to_image/expected_github.json @@ -0,0 +1,102 @@ +{ + "description": "Provides integration with GitHub's APIs", + "tier": "Official", + "status": "Active", + "transport": "stdio", + "tools": [ + "add_comment_to_pending_review", + "add_issue_comment", + "add_sub_issue", + "assign_copilot_to_issue", + "create_branch", + "create_issue", + "create_or_update_file", + "create_pull_request", + "create_repository", + "delete_file", + "fork_repository", + "get_commit", + "get_file_contents", + "get_issue", + "get_issue_comments", + "get_label", + "get_latest_release", + "get_me", + "get_release_by_tag", + "get_tag", + "get_team_members", + "get_teams", + "list_branches", + "list_commits", + "list_issue_types", + "list_issues", + "list_label", + "list_pull_requests", + "list_releases", + "list_sub_issues", + "list_tags", + "merge_pull_request", + "pull_request_read", + "pull_request_review_write", + "push_files", + "remove_sub_issue", + "reprioritize_sub_issue", + "request_copilot_review", + "search_code", + "search_issues", + "search_pull_requests", + "search_repositories", + "search_users", + "update_issue", + "update_pull_request", + "update_pull_request_branch" + ], + "metadata": { + "stars": 23700, + "pulls": 5000, + "last_updated": "2025-10-18T02:26:51Z" + }, + "repository_url": "https://github.com/github/github-mcp-server", + "tags": [ + "api", + "create", + "fork", + "github", + "list", + "pull-request", + "push", + "repository", + "search", + "update", + "issues" + ], + "image": "ghcr.io/github/github-mcp-server:v0.19.1", + "env_vars": [ + { + "name": "GITHUB_PERSONAL_ACCESS_TOKEN", + "description": "GitHub personal access token with appropriate permissions", + "required": true, + "secret": true + }, + { + "name": "GITHUB_HOST", + "description": "GitHub Enterprise Server hostname (optional)", + "required": false + }, + { + "name": "GITHUB_TOOLSETS", + "description": "Comma-separated list of toolsets to enable (e.g., 'repos,issues,pull_requests'). If not set, all toolsets are enabled. See the README for available toolsets.", + "required": false + }, + { + "name": "GITHUB_DYNAMIC_TOOLSETS", + "description": "Set to '1' to enable dynamic toolset discovery", + "required": false + }, + { + "name": "GITHUB_READ_ONLY", + "description": "Set to '1' to enable read-only mode, preventing any modifications to GitHub resources", + "required": false + } + ] +} diff --git a/pkg/registry/converters/testdata/server_to_image/input_github.json b/pkg/registry/converters/testdata/server_to_image/input_github.json new file mode 100644 index 000000000..1fc2cbc24 --- /dev/null +++ b/pkg/registry/converters/testdata/server_to_image/input_github.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.stacklok/github", + "description": "Provides integration with GitHub's APIs", + "repository": { + "url": "https://github.com/github/github-mcp-server", + "source": "github" + }, + "version": "1.0.0", + "packages": [ + { + "registryType": "oci", + "identifier": "ghcr.io/github/github-mcp-server:v0.19.1", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "description": "GitHub personal access token with appropriate permissions", + "isRequired": true, + "isSecret": true, + "name": "GITHUB_PERSONAL_ACCESS_TOKEN" + }, + { + "description": "GitHub Enterprise Server hostname (optional)", + "name": "GITHUB_HOST" + }, + { + "description": "Comma-separated list of toolsets to enable (e.g., 'repos,issues,pull_requests'). If not set, all toolsets are enabled. See the README for available toolsets.", + "name": "GITHUB_TOOLSETS" + }, + { + "description": "Set to '1' to enable dynamic toolset discovery", + "name": "GITHUB_DYNAMIC_TOOLSETS" + }, + { + "description": "Set to '1' to enable read-only mode, preventing any modifications to GitHub resources", + "name": "GITHUB_READ_ONLY" + } + ] + } + ], + "_meta": { + "io.modelcontextprotocol.registry/publisher-provided": { + "io.github.stacklok": { + "ghcr.io/github/github-mcp-server:v0.19.1": { + "metadata": { + "last_updated": "2025-10-18T02:26:51Z", + "pulls": 5000, + "stars": 23700 + }, + "status": "Active", + "tags": [ + "api", + "create", + "fork", + "github", + "list", + "pull-request", + "push", + "repository", + "search", + "update", + "issues" + ], + "tier": "Official", + "tools": [ + "add_comment_to_pending_review", + "add_issue_comment", + "add_sub_issue", + "assign_copilot_to_issue", + "create_branch", + "create_issue", + "create_or_update_file", + "create_pull_request", + "create_repository", + "delete_file", + "fork_repository", + "get_commit", + "get_file_contents", + "get_issue", + "get_issue_comments", + "get_label", + "get_latest_release", + "get_me", + "get_release_by_tag", + "get_tag", + "get_team_members", + "get_teams", + "list_branches", + "list_commits", + "list_issue_types", + "list_issues", + "list_label", + "list_pull_requests", + "list_releases", + "list_sub_issues", + "list_tags", + "merge_pull_request", + "pull_request_read", + "pull_request_review_write", + "push_files", + "remove_sub_issue", + "reprioritize_sub_issue", + "request_copilot_review", + "search_code", + "search_issues", + "search_pull_requests", + "search_repositories", + "search_users", + "update_issue", + "update_pull_request", + "update_pull_request_branch" + ], + "transport": "stdio" + } + } + } + } +} diff --git a/pkg/registry/converters/testdata/server_to_remote/expected_example.json b/pkg/registry/converters/testdata/server_to_remote/expected_example.json new file mode 100644 index 000000000..24b06f5ff --- /dev/null +++ b/pkg/registry/converters/testdata/server_to_remote/expected_example.json @@ -0,0 +1,36 @@ +{ + "description": "Example remote MCP server accessed via SSE", + "tier": "Community", + "status": "active", + "transport": "sse", + "tools": [ + "get_data", + "send_notification", + "query_api" + ], + "metadata": { + "stars": 150, + "pulls": 500, + "last_updated": "2025-10-20T10:00:00Z" + }, + "repository_url": "https://github.com/example/remote-mcp-server", + "tags": [ + "remote", + "sse", + "api" + ], + "url": "https://api.example.com/mcp", + "headers": [ + { + "name": "Authorization", + "description": "Bearer token for API authentication", + "required": true, + "secret": true + }, + { + "name": "X-API-Version", + "description": "API version to use", + "required": false + } + ] +} diff --git a/pkg/registry/converters/testdata/server_to_remote/input_example.json b/pkg/registry/converters/testdata/server_to_remote/input_example.json new file mode 100644 index 000000000..91dd52cd7 --- /dev/null +++ b/pkg/registry/converters/testdata/server_to_remote/input_example.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.stacklok/example-remote", + "description": "Example remote MCP server accessed via SSE", + "repository": { + "url": "https://github.com/example/remote-mcp-server", + "source": "github" + }, + "version": "1.0.0", + "remotes": [ + { + "type": "sse", + "url": "https://api.example.com/mcp", + "headers": [ + { + "description": "Bearer token for API authentication", + "isRequired": true, + "isSecret": true, + "name": "Authorization" + }, + { + "description": "API version to use", + "name": "X-API-Version" + } + ] + } + ], + "_meta": { + "io.modelcontextprotocol.registry/publisher-provided": { + "io.github.stacklok": { + "https://api.example.com/mcp": { + "metadata": { + "last_updated": "2025-10-20T10:00:00Z", + "pulls": 500, + "stars": 150 + }, + "status": "active", + "tags": [ + "remote", + "sse", + "api" + ], + "tier": "Community", + "tools": [ + "get_data", + "send_notification", + "query_api" + ], + "transport": "sse" + } + } + } + } +} diff --git a/pkg/registry/converters/toolhive_to_upstream.go b/pkg/registry/converters/toolhive_to_upstream.go new file mode 100644 index 000000000..d920cecc5 --- /dev/null +++ b/pkg/registry/converters/toolhive_to_upstream.go @@ -0,0 +1,287 @@ +// Package converters provides bidirectional conversion between toolhive registry formats +// and the upstream MCP (Model Context Protocol) ServerJSON format. +// +// The package supports two conversion directions: +// - toolhive → upstream: ImageMetadata/RemoteServerMetadata → ServerJSON (this file) +// - upstream → toolhive: ServerJSON → ImageMetadata/RemoteServerMetadata (upstream_to_toolhive.go) +// +// Toolhive-specific fields (permissions, provenance, metadata) are stored in the upstream +// format's publisher extensions under "io.github.stacklok", allowing additional metadata +// while maintaining compatibility with the standard MCP registry format. +package converters + +import ( + "fmt" + + upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stacklok/toolhive/pkg/registry/types" +) + +// ImageMetadataToServerJSON converts toolhive ImageMetadata to an upstream ServerJSON +// The name parameter should be the simple server name (e.g., "fetch") +func ImageMetadataToServerJSON(name string, imageMetadata *types.ImageMetadata) (*upstream.ServerJSON, error) { + if imageMetadata == nil { + return nil, fmt.Errorf("imageMetadata cannot be nil") + } + if name == "" { + return nil, fmt.Errorf("name cannot be empty") + } + + // Create ServerJSON with basic fields + serverJSON := &upstream.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: BuildReverseDNSName(name), + Title: imageMetadata.Name, + Description: imageMetadata.Description, + Version: "1.0.0", // TODO: Extract from image tag or metadata + } + + // Set repository if available + if imageMetadata.RepositoryURL != "" { + serverJSON.Repository = &model.Repository{ + URL: imageMetadata.RepositoryURL, + Source: "github", // Assume GitHub + } + } + + // Create package + serverJSON.Packages = createPackagesFromImageMetadata(imageMetadata) + + // Create publisher extensions + serverJSON.Meta = &upstream.ServerMeta{ + PublisherProvided: createImageExtensions(imageMetadata), + } + + return serverJSON, nil +} + +// RemoteServerMetadataToServerJSON converts toolhive RemoteServerMetadata to an upstream ServerJSON +// The name parameter should be the simple server name (e.g., "github-remote") +func RemoteServerMetadataToServerJSON(name string, remoteMetadata *types.RemoteServerMetadata) (*upstream.ServerJSON, error) { + if remoteMetadata == nil { + return nil, fmt.Errorf("remoteMetadata cannot be nil") + } + if name == "" { + return nil, fmt.Errorf("name cannot be empty") + } + + // Create ServerJSON with basic fields + serverJSON := &upstream.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: BuildReverseDNSName(name), + Title: remoteMetadata.Name, + Description: remoteMetadata.Description, + Version: "1.0.0", // TODO: Version management + } + + // Set repository if available + if remoteMetadata.RepositoryURL != "" { + serverJSON.Repository = &model.Repository{ + URL: remoteMetadata.RepositoryURL, + Source: "github", // Assume GitHub + } + } + + // Create remote + serverJSON.Remotes = createRemotesFromRemoteMetadata(remoteMetadata) + + // Create publisher extensions + serverJSON.Meta = &upstream.ServerMeta{ + PublisherProvided: createRemoteExtensions(remoteMetadata), + } + + return serverJSON, nil +} + +// createPackagesFromImageMetadata creates OCI Package entries from ImageMetadata +func createPackagesFromImageMetadata(imageMetadata *types.ImageMetadata) []model.Package { + // Convert environment variables + var envVars []model.KeyValueInput + for _, envVar := range imageMetadata.EnvVars { + envVars = append(envVars, model.KeyValueInput{ + Name: envVar.Name, + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: envVar.Description, + IsRequired: envVar.Required, + IsSecret: envVar.Secret, + Default: envVar.Default, + }, + }, + }) + } + + // Determine transport + transportType := imageMetadata.Transport + if transportType == "" { + transportType = model.TransportTypeStdio + } + + transport := model.Transport{ + Type: transportType, + } + + // Add URL for non-stdio transports + // Note: We use localhost as the host because container-based MCP servers run locally + // and are accessed via port forwarding. The actual container may listen on 0.0.0.0, + // but clients connect via localhost on the host machine. + if transportType == model.TransportTypeStreamableHTTP || transportType == model.TransportTypeSSE { + if imageMetadata.TargetPort > 0 { + // Include port in URL if explicitly set + transport.URL = fmt.Sprintf("http://localhost:%d", imageMetadata.TargetPort) + } else { + // No port specified - use URL without port (standard HTTP port 80) + transport.URL = "http://localhost" + } + } + + return []model.Package{{ + RegistryType: model.RegistryTypeOCI, + Identifier: imageMetadata.Image, + EnvironmentVariables: envVars, + Transport: transport, + }} +} + +// createRemotesFromRemoteMetadata creates Transport entries from RemoteServerMetadata +func createRemotesFromRemoteMetadata(remoteMetadata *types.RemoteServerMetadata) []model.Transport { + // Convert headers + var headers []model.KeyValueInput + for _, header := range remoteMetadata.Headers { + headers = append(headers, model.KeyValueInput{ + Name: header.Name, + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: header.Description, + IsRequired: header.Required, + IsSecret: header.Secret, + }, + }, + }) + } + + return []model.Transport{{ + Type: remoteMetadata.Transport, + URL: remoteMetadata.URL, + Headers: headers, + }} +} + +// createImageExtensions creates publisher extensions map from ImageMetadata +func createImageExtensions(imageMetadata *types.ImageMetadata) map[string]interface{} { + extensions := make(map[string]interface{}) + + // Always include status + extensions["status"] = imageMetadata.Status + if extensions["status"] == "" { + extensions["status"] = "active" + } + + // Add tools + if len(imageMetadata.Tools) > 0 { + tools := make([]interface{}, len(imageMetadata.Tools)) + for i, tool := range imageMetadata.Tools { + tools[i] = tool + } + extensions["tools"] = tools + } + + // Add tier + if imageMetadata.Tier != "" { + extensions["tier"] = imageMetadata.Tier + } + + // Add tags + if len(imageMetadata.Tags) > 0 { + tags := make([]interface{}, len(imageMetadata.Tags)) + for i, tag := range imageMetadata.Tags { + tags[i] = tag + } + extensions["tags"] = tags + } + + // Add metadata + if imageMetadata.Metadata != nil { + extensions["metadata"] = map[string]interface{}{ + "stars": float64(imageMetadata.Metadata.Stars), + "pulls": float64(imageMetadata.Metadata.Pulls), + "last_updated": imageMetadata.Metadata.LastUpdated, + } + } + + // Add permissions + if imageMetadata.Permissions != nil { + extensions["permissions"] = imageMetadata.Permissions + } + + // Add args (static container arguments) + if len(imageMetadata.Args) > 0 { + extensions["args"] = imageMetadata.Args + } + + // Add provenance + if imageMetadata.Provenance != nil { + extensions["provenance"] = imageMetadata.Provenance + } + + return map[string]interface{}{ + "io.github.stacklok": map[string]interface{}{ + imageMetadata.Image: extensions, + }, + } +} + +// createRemoteExtensions creates publisher extensions map from RemoteServerMetadata +func createRemoteExtensions(remoteMetadata *types.RemoteServerMetadata) map[string]interface{} { + extensions := make(map[string]interface{}) + + // Always include status + extensions["status"] = remoteMetadata.Status + if extensions["status"] == "" { + extensions["status"] = "active" + } + + // Add tools + if len(remoteMetadata.Tools) > 0 { + tools := make([]interface{}, len(remoteMetadata.Tools)) + for i, tool := range remoteMetadata.Tools { + tools[i] = tool + } + extensions["tools"] = tools + } + + // Add tier + if remoteMetadata.Tier != "" { + extensions["tier"] = remoteMetadata.Tier + } + + // Add tags + if len(remoteMetadata.Tags) > 0 { + tags := make([]interface{}, len(remoteMetadata.Tags)) + for i, tag := range remoteMetadata.Tags { + tags[i] = tag + } + extensions["tags"] = tags + } + + // Add metadata + if remoteMetadata.Metadata != nil { + extensions["metadata"] = map[string]interface{}{ + "stars": float64(remoteMetadata.Metadata.Stars), + "pulls": float64(remoteMetadata.Metadata.Pulls), + "last_updated": remoteMetadata.Metadata.LastUpdated, + } + } + + // Add OAuth config + if remoteMetadata.OAuthConfig != nil { + extensions["oauth_config"] = remoteMetadata.OAuthConfig + } + + return map[string]interface{}{ + "io.github.stacklok": map[string]interface{}{ + remoteMetadata.URL: extensions, + }, + } +} diff --git a/pkg/registry/converters.go b/pkg/registry/converters/upstream_to_toolhive.go similarity index 78% rename from pkg/registry/converters.go rename to pkg/registry/converters/upstream_to_toolhive.go index 36f867736..52ebdf593 100644 --- a/pkg/registry/converters.go +++ b/pkg/registry/converters/upstream_to_toolhive.go @@ -1,29 +1,22 @@ -// Package registry provides conversion functions from upstream MCP ServerJSON format +// Package converters provides conversion functions from upstream MCP ServerJSON format // to toolhive ImageMetadata/RemoteServerMetadata formats. -// -// TEMPORARY COPY: This file contains converters copied from github.com/stacklok/toolhive-registry -// to avoid circular dependency issues. This is a temporary solution. -// -// TODO: Refactor to move types to toolhive-registry and import converters from there. -// See: https://github.com/stacklok/toolhive/issues/XXXX -package registry +package converters import ( "encoding/json" "fmt" "net/url" "strconv" - "strings" upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" - "github.com/stacklok/toolhive/pkg/permissions" + "github.com/stacklok/toolhive/pkg/registry/types" ) // ServerJSONToImageMetadata converts an upstream ServerJSON (with OCI packages) to toolhive ImageMetadata // This function only handles OCI packages and will error if there are multiple OCI packages -func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*ImageMetadata, error) { +func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*types.ImageMetadata, error) { if serverJSON == nil { return nil, fmt.Errorf("serverJSON cannot be nil") } @@ -33,15 +26,9 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*ImageMetadata, return nil, err } - // Use Title if present, otherwise extract from reverse-DNS Name - name := serverJSON.Title - if name == "" { - name = ExtractServerName(serverJSON.Name) - } - - imageMetadata := &ImageMetadata{ - BaseServerMetadata: BaseServerMetadata{ - Name: name, + imageMetadata := &types.ImageMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ + Name: serverJSON.Title, Description: serverJSON.Description, Transport: pkg.Transport.Type, }, @@ -54,7 +41,7 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*ImageMetadata, } // Convert environment variables - imageMetadata.EnvVars = convertMCPEnvironmentVariables(pkg.EnvironmentVariables) + imageMetadata.EnvVars = convertEnvironmentVariables(pkg.EnvironmentVariables) // Extract target port from transport URL if present imageMetadata.TargetPort = extractTargetPort(pkg.Transport.URL, serverJSON.Name) @@ -97,15 +84,15 @@ func extractSingleOCIPackage(serverJSON *upstream.ServerJSON) (model.Package, er return ociPackages[0], nil } -// convertMCPEnvironmentVariables converts model.KeyValueInput to EnvVar (from MCP Registry API) -func convertMCPEnvironmentVariables(envVars []model.KeyValueInput) []*EnvVar { +// convertEnvironmentVariables converts model.KeyValueInput to types.EnvVar +func convertEnvironmentVariables(envVars []model.KeyValueInput) []*types.EnvVar { if len(envVars) == 0 { return nil } - result := make([]*EnvVar, 0, len(envVars)) + result := make([]*types.EnvVar, 0, len(envVars)) for _, envVar := range envVars { - result = append(result, &EnvVar{ + result = append(result, &types.EnvVar{ Name: envVar.Name, Description: envVar.Description, Required: envVar.IsRequired, @@ -145,7 +132,7 @@ func extractTargetPort(transportURL, serverName string) int { // ServerJSONToRemoteServerMetadata converts an upstream ServerJSON (with remotes) to toolhive RemoteServerMetadata // This function extracts remote server data and reconstructs RemoteServerMetadata format -func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteServerMetadata, error) { +func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*types.RemoteServerMetadata, error) { if serverJSON == nil { return nil, fmt.Errorf("serverJSON cannot be nil") } @@ -156,15 +143,9 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteS remote := serverJSON.Remotes[0] // Use first remote - // Use Title if present, otherwise extract from reverse-DNS Name - name := serverJSON.Title - if name == "" { - name = ExtractServerName(serverJSON.Name) - } - - remoteMetadata := &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ - Name: name, + remoteMetadata := &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ + Name: serverJSON.Title, Description: serverJSON.Description, Transport: remote.Type, }, @@ -178,9 +159,9 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteS // Convert headers if len(remote.Headers) > 0 { - remoteMetadata.Headers = make([]*Header, 0, len(remote.Headers)) + remoteMetadata.Headers = make([]*types.Header, 0, len(remote.Headers)) for _, header := range remote.Headers { - remoteMetadata.Headers = append(remoteMetadata.Headers, &Header{ + remoteMetadata.Headers = append(remoteMetadata.Headers, &types.Header{ Name: header.Name, Description: header.Description, Required: header.IsRequired, @@ -196,7 +177,7 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*RemoteS } // extractImageExtensions extracts publisher-provided extensions into ImageMetadata -func extractImageExtensions(serverJSON *upstream.ServerJSON, imageMetadata *ImageMetadata) { +func extractImageExtensions(serverJSON *upstream.ServerJSON, imageMetadata *types.ImageMetadata) { extensions := getStacklokExtensions(serverJSON) if extensions == nil { return @@ -228,7 +209,7 @@ func getStacklokExtensions(serverJSON *upstream.ServerJSON) map[string]interface } // extractBasicImageFields extracts basic string and slice fields -func extractBasicImageFields(extensions map[string]interface{}, imageMetadata *ImageMetadata) { +func extractBasicImageFields(extensions map[string]interface{}, imageMetadata *types.ImageMetadata) { if status, ok := extensions["status"].(string); ok { imageMetadata.Status = status } @@ -244,13 +225,13 @@ func extractBasicImageFields(extensions map[string]interface{}, imageMetadata *I } // extractImageMetadataField extracts the metadata object (stars, pulls, last_updated) -func extractImageMetadataField(extensions map[string]interface{}, imageMetadata *ImageMetadata) { +func extractImageMetadataField(extensions map[string]interface{}, imageMetadata *types.ImageMetadata) { metadataData, ok := extensions["metadata"].(map[string]interface{}) if !ok { return } - imageMetadata.Metadata = &Metadata{} + imageMetadata.Metadata = &types.Metadata{} if stars, ok := metadataData["stars"].(float64); ok { imageMetadata.Metadata.Stars = int(stars) } @@ -263,7 +244,7 @@ func extractImageMetadataField(extensions map[string]interface{}, imageMetadata } // extractComplexImageFields extracts complex fields (args, permissions, provenance) -func extractComplexImageFields(extensions map[string]interface{}, imageMetadata *ImageMetadata) { +func extractComplexImageFields(extensions map[string]interface{}, imageMetadata *types.ImageMetadata) { // Extract args (fallback if PackageArguments wasn't used) if len(imageMetadata.Args) == 0 { if argsData, ok := extensions["args"].([]interface{}); ok { @@ -278,12 +259,12 @@ func extractComplexImageFields(extensions map[string]interface{}, imageMetadata // Extract provenance using JSON round-trip if provData, ok := extensions["provenance"]; ok { - imageMetadata.Provenance = remarshalToType[*Provenance](provData) + imageMetadata.Provenance = remarshalToType[*types.Provenance](provData) } } // extractRemoteExtensions extracts publisher-provided extensions into RemoteServerMetadata -func extractRemoteExtensions(serverJSON *upstream.ServerJSON, remoteMetadata *RemoteServerMetadata) { +func extractRemoteExtensions(serverJSON *upstream.ServerJSON, remoteMetadata *types.RemoteServerMetadata) { if serverJSON.Meta == nil || serverJSON.Meta.PublisherProvided == nil { return } @@ -314,7 +295,7 @@ func extractRemoteExtensions(serverJSON *upstream.ServerJSON, remoteMetadata *Re remoteMetadata.Tags = interfaceSliceToStringSlice(tagsData) } if metadataData, ok := extensions["metadata"].(map[string]interface{}); ok { - remoteMetadata.Metadata = &Metadata{} + remoteMetadata.Metadata = &types.Metadata{} if stars, ok := metadataData["stars"].(float64); ok { remoteMetadata.Metadata.Stars = int(stars) } @@ -328,7 +309,7 @@ func extractRemoteExtensions(serverJSON *upstream.ServerJSON, remoteMetadata *Re // Extract OAuth config using JSON round-trip if oauthData, ok := extensions["oauth_config"]; ok { - remoteMetadata.OAuthConfig = remarshalToType[*OAuthConfig](oauthData) + remoteMetadata.OAuthConfig = remarshalToType[*types.OAuthConfig](oauthData) } break // Only process first entry @@ -368,33 +349,3 @@ func flattenPackageArguments(args []model.Argument) []string { } return result } - -// interfaceSliceToStringSlice converts []interface{} to []string -func interfaceSliceToStringSlice(input []interface{}) []string { - result := make([]string, 0, len(input)) - for _, item := range input { - if str, ok := item.(string); ok { - result = append(result, str) - } - } - return result -} - -// ExtractServerName extracts the simple server name from a reverse-DNS format name -// Example: "io.github.stacklok/fetch" -> "fetch" -func ExtractServerName(reverseDNSName string) string { - parts := strings.Split(reverseDNSName, "/") - if len(parts) == 2 { - return parts[1] - } - return reverseDNSName -} - -// BuildReverseDNSName builds a reverse-DNS format name from a simple name -// Example: "fetch" -> "io.github.stacklok/fetch" -func BuildReverseDNSName(simpleName string) string { - if strings.Contains(simpleName, "/") { - return simpleName // Already in reverse-DNS format - } - return "io.github.stacklok/" + simpleName -} diff --git a/pkg/registry/converters/utils.go b/pkg/registry/converters/utils.go new file mode 100644 index 000000000..ee14057cf --- /dev/null +++ b/pkg/registry/converters/utils.go @@ -0,0 +1,36 @@ +// Package converters provides utility functions for conversion between upstream and toolhive formats. +package converters + +import ( + "strings" +) + +// interfaceSliceToStringSlice converts []interface{} to []string +func interfaceSliceToStringSlice(input []interface{}) []string { + result := make([]string, 0, len(input)) + for _, item := range input { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result +} + +// ExtractServerName extracts the simple server name from a reverse-DNS format name +// Example: "io.github.stacklok/fetch" -> "fetch" +func ExtractServerName(reverseDNSName string) string { + parts := strings.Split(reverseDNSName, "/") + if len(parts) == 2 { + return parts[1] + } + return reverseDNSName +} + +// BuildReverseDNSName builds a reverse-DNS format name from a simple name +// Example: "fetch" -> "io.github.stacklok/fetch" +func BuildReverseDNSName(simpleName string) string { + if strings.Contains(simpleName, "/") { + return simpleName // Already in reverse-DNS format + } + return "io.github.stacklok/" + simpleName +} diff --git a/pkg/registry/mocks/mock_provider.go b/pkg/registry/mocks/mock_provider.go index b34d424ff..28c2b2491 100644 --- a/pkg/registry/mocks/mock_provider.go +++ b/pkg/registry/mocks/mock_provider.go @@ -12,7 +12,7 @@ package mocks import ( reflect "reflect" - registry "github.com/stacklok/toolhive/pkg/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -41,10 +41,10 @@ func (m *MockProvider) EXPECT() *MockProviderMockRecorder { } // GetImageServer mocks base method. -func (m *MockProvider) GetImageServer(name string) (*registry.ImageMetadata, error) { +func (m *MockProvider) GetImageServer(name string) (*types.ImageMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetImageServer", name) - ret0, _ := ret[0].(*registry.ImageMetadata) + ret0, _ := ret[0].(*types.ImageMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -56,10 +56,10 @@ func (mr *MockProviderMockRecorder) GetImageServer(name any) *gomock.Call { } // GetRegistry mocks base method. -func (m *MockProvider) GetRegistry() (*registry.Registry, error) { +func (m *MockProvider) GetRegistry() (*types.Registry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistry") - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*types.Registry) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -71,10 +71,10 @@ func (mr *MockProviderMockRecorder) GetRegistry() *gomock.Call { } // GetServer mocks base method. -func (m *MockProvider) GetServer(name string) (registry.ServerMetadata, error) { +func (m *MockProvider) GetServer(name string) (types.ServerMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetServer", name) - ret0, _ := ret[0].(registry.ServerMetadata) + ret0, _ := ret[0].(types.ServerMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -86,10 +86,10 @@ func (mr *MockProviderMockRecorder) GetServer(name any) *gomock.Call { } // ListImageServers mocks base method. -func (m *MockProvider) ListImageServers() ([]*registry.ImageMetadata, error) { +func (m *MockProvider) ListImageServers() ([]*types.ImageMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListImageServers") - ret0, _ := ret[0].([]*registry.ImageMetadata) + ret0, _ := ret[0].([]*types.ImageMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -101,10 +101,10 @@ func (mr *MockProviderMockRecorder) ListImageServers() *gomock.Call { } // ListServers mocks base method. -func (m *MockProvider) ListServers() ([]registry.ServerMetadata, error) { +func (m *MockProvider) ListServers() ([]types.ServerMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListServers") - ret0, _ := ret[0].([]registry.ServerMetadata) + ret0, _ := ret[0].([]types.ServerMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -116,10 +116,10 @@ func (mr *MockProviderMockRecorder) ListServers() *gomock.Call { } // SearchImageServers mocks base method. -func (m *MockProvider) SearchImageServers(query string) ([]*registry.ImageMetadata, error) { +func (m *MockProvider) SearchImageServers(query string) ([]*types.ImageMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchImageServers", query) - ret0, _ := ret[0].([]*registry.ImageMetadata) + ret0, _ := ret[0].([]*types.ImageMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -131,10 +131,10 @@ func (mr *MockProviderMockRecorder) SearchImageServers(query any) *gomock.Call { } // SearchServers mocks base method. -func (m *MockProvider) SearchServers(query string) ([]registry.ServerMetadata, error) { +func (m *MockProvider) SearchServers(query string) ([]types.ServerMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchServers", query) - ret0, _ := ret[0].([]registry.ServerMetadata) + ret0, _ := ret[0].([]types.ServerMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/registry/provider.go b/pkg/registry/provider.go index 2888f4297..9f708a5fc 100644 --- a/pkg/registry/provider.go +++ b/pkg/registry/provider.go @@ -1,28 +1,30 @@ package registry +import "github.com/stacklok/toolhive/pkg/registry/types" + //go:generate mockgen -destination=mocks/mock_provider.go -package=mocks -source=provider.go Provider // Provider defines the interface for registry storage implementations type Provider interface { // GetRegistry returns the complete registry data - GetRegistry() (*Registry, error) + GetRegistry() (*types.Registry, error) // GetServer returns a specific server by name (container or remote) - GetServer(name string) (ServerMetadata, error) + GetServer(name string) (types.ServerMetadata, error) // SearchServers searches for servers matching the query (both container and remote) - SearchServers(query string) ([]ServerMetadata, error) + SearchServers(query string) ([]types.ServerMetadata, error) // ListServers returns all available servers (both container and remote) - ListServers() ([]ServerMetadata, error) + ListServers() ([]types.ServerMetadata, error) // Legacy methods for backward compatibility // GetImageServer returns a specific container server by name - GetImageServer(name string) (*ImageMetadata, error) + GetImageServer(name string) (*types.ImageMetadata, error) // SearchImageServers searches for container servers matching the query - SearchImageServers(query string) ([]*ImageMetadata, error) + SearchImageServers(query string) ([]*types.ImageMetadata, error) // ListImageServers returns all available container servers - ListImageServers() ([]*ImageMetadata, error) + ListImageServers() ([]*types.ImageMetadata, error) } diff --git a/pkg/registry/provider_api.go b/pkg/registry/provider_api.go index 6c6dbf472..6bb85224f 100644 --- a/pkg/registry/provider_api.go +++ b/pkg/registry/provider_api.go @@ -9,12 +9,10 @@ import ( v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/stacklok/toolhive/pkg/registry/api" + "github.com/stacklok/toolhive/pkg/registry/converters" + "github.com/stacklok/toolhive/pkg/registry/types" ) -// NOTE: Using converters from converters.go (same package) to avoid circular dependency. -// This is a TEMPORARY solution - converters are copied from toolhive-registry. -// TODO: Move types to toolhive-registry and import converters as a library. - // APIRegistryProvider provides registry data from an MCP Registry API endpoint // It queries the API on-demand for each operation, ensuring fresh data. type APIRegistryProvider struct { @@ -55,7 +53,7 @@ func NewAPIRegistryProvider(apiURL string, allowPrivateIp bool) (*APIRegistryPro // GetRegistry returns the registry data by fetching all servers from the API // This method queries the API and converts all servers to ToolHive format. // Note: This can be slow for large registries as it fetches everything. -func (p *APIRegistryProvider) GetRegistry() (*Registry, error) { +func (p *APIRegistryProvider) GetRegistry() (*types.Registry, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -72,22 +70,22 @@ func (p *APIRegistryProvider) GetRegistry() (*Registry, error) { } // Build Registry structure - registry := &Registry{ + registry := &types.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*ImageMetadata), - RemoteServers: make(map[string]*RemoteServerMetadata), - Groups: []*Group{}, + Servers: make(map[string]*types.ImageMetadata), + RemoteServers: make(map[string]*types.RemoteServerMetadata), + Groups: []*types.Group{}, } // Separate servers into container and remote for _, server := range serverMetadata { if server.IsRemote() { - if remoteServer, ok := server.(*RemoteServerMetadata); ok { + if remoteServer, ok := server.(*types.RemoteServerMetadata); ok { registry.RemoteServers[remoteServer.Name] = remoteServer } } else { - if imageServer, ok := server.(*ImageMetadata); ok { + if imageServer, ok := server.(*types.ImageMetadata); ok { registry.Servers[imageServer.Name] = imageServer } } @@ -97,13 +95,13 @@ func (p *APIRegistryProvider) GetRegistry() (*Registry, error) { } // GetServer returns a specific server by name (queries API directly) -func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { +func (p *APIRegistryProvider) GetServer(name string) (types.ServerMetadata, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Try direct API lookup first (supports both reverse-DNS and simple names) // Build potential reverse-DNS name - reverseDNSName := BuildReverseDNSName(name) + reverseDNSName := converters.BuildReverseDNSName(name) // Try the reverse-DNS format first serverJSON, err := p.client.GetServer(ctx, reverseDNSName) @@ -127,7 +125,7 @@ func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { // Find exact match in search results for _, server := range servers { - simpleName := ExtractServerName(server.Name) + simpleName := converters.ExtractServerName(server.Name) if simpleName == name || server.Name == name { return ConvertServerJSON(server) } @@ -137,7 +135,7 @@ func (p *APIRegistryProvider) GetServer(name string) (ServerMetadata, error) { } // SearchServers searches for servers matching the query (queries API directly) -func (p *APIRegistryProvider) SearchServers(query string) ([]ServerMetadata, error) { +func (p *APIRegistryProvider) SearchServers(query string) ([]types.ServerMetadata, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -151,7 +149,7 @@ func (p *APIRegistryProvider) SearchServers(query string) ([]ServerMetadata, err } // ListServers returns all servers from the API -func (p *APIRegistryProvider) ListServers() ([]ServerMetadata, error) { +func (p *APIRegistryProvider) ListServers() ([]types.ServerMetadata, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -166,14 +164,14 @@ func (p *APIRegistryProvider) ListServers() ([]ServerMetadata, error) { // GetImageServer returns a specific container server by name (overrides BaseProvider) // This override is necessary because BaseProvider.GetImageServer calls p.GetServer, // which would call BaseProvider.GetServer instead of APIRegistryProvider.GetServer -func (p *APIRegistryProvider) GetImageServer(name string) (*ImageMetadata, error) { +func (p *APIRegistryProvider) GetImageServer(name string) (*types.ImageMetadata, error) { server, err := p.GetServer(name) if err != nil { return nil, err } // Type assert to ImageMetadata - if img, ok := server.(*ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { return img, nil } @@ -183,7 +181,7 @@ func (p *APIRegistryProvider) GetImageServer(name string) (*ImageMetadata, error // ConvertServerJSON converts an MCP Registry API ServerJSON to ToolHive ServerMetadata // Uses converters from converters.go (same package) // Note: Only handles OCI packages and remote servers, skips npm/pypi by design -func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { +func ConvertServerJSON(serverJSON *v0.ServerJSON) (types.ServerMetadata, error) { if serverJSON == nil { return nil, fmt.Errorf("serverJSON is nil") } @@ -191,17 +189,17 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { // Determine if this is a remote server or container-based server // Remote servers have the 'remotes' field populated // Container servers have the 'packages' field populated - var result ServerMetadata + var result types.ServerMetadata var err error if len(serverJSON.Remotes) > 0 { - result, err = ServerJSONToRemoteServerMetadata(serverJSON) + result, err = converters.ServerJSONToRemoteServerMetadata(serverJSON) } else if len(serverJSON.Packages) == 0 { // Skip servers without packages or remotes (incomplete entries) return nil, fmt.Errorf("server %s has no packages or remotes, skipping", serverJSON.Name) } else { // ServerJSONToImageMetadata only handles OCI packages, will error on npm/pypi - result, err = ServerJSONToImageMetadata(serverJSON) + result, err = converters.ServerJSONToImageMetadata(serverJSON) } if err != nil { @@ -218,8 +216,8 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (ServerMetadata, error) { // ConvertServersToMetadata converts a slice of ServerJSON to a slice of ServerMetadata // Skips servers that cannot be converted (e.g., incomplete entries) // Uses official converters from toolhive-registry package -func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]ServerMetadata, error) { - result := make([]ServerMetadata, 0, len(servers)) +func ConvertServersToMetadata(servers []*v0.ServerJSON) ([]types.ServerMetadata, error) { + result := make([]types.ServerMetadata, 0, len(servers)) for _, server := range servers { metadata, err := ConvertServerJSON(server) diff --git a/pkg/registry/provider_base.go b/pkg/registry/provider_base.go index a87e6013f..fcaa699b3 100644 --- a/pkg/registry/provider_base.go +++ b/pkg/registry/provider_base.go @@ -3,24 +3,26 @@ package registry import ( "fmt" "strings" + + "github.com/stacklok/toolhive/pkg/registry/types" ) // BaseProvider provides common implementation for registry providers type BaseProvider struct { // GetRegistryFunc is a function that fetches the registry data // This allows different providers to implement their own data fetching logic - GetRegistryFunc func() (*Registry, error) + GetRegistryFunc func() (*types.Registry, error) } // NewBaseProvider creates a new base provider with the given registry function -func NewBaseProvider(getRegistry func() (*Registry, error)) *BaseProvider { +func NewBaseProvider(getRegistry func() (*types.Registry, error)) *BaseProvider { return &BaseProvider{ GetRegistryFunc: getRegistry, } } // GetServer returns a specific server by name (container or remote) -func (p *BaseProvider) GetServer(name string) (ServerMetadata, error) { +func (p *BaseProvider) GetServer(name string) (types.ServerMetadata, error) { reg, err := p.GetRegistryFunc() if err != nil { return nil, err @@ -36,14 +38,14 @@ func (p *BaseProvider) GetServer(name string) (ServerMetadata, error) { } // SearchServers searches for servers matching the query (both container and remote) -func (p *BaseProvider) SearchServers(query string) ([]ServerMetadata, error) { +func (p *BaseProvider) SearchServers(query string) ([]types.ServerMetadata, error) { reg, err := p.GetRegistryFunc() if err != nil { return nil, err } query = strings.ToLower(query) - var results []ServerMetadata + var results []types.ServerMetadata // Search container servers for name, server := range reg.Servers { @@ -63,7 +65,7 @@ func (p *BaseProvider) SearchServers(query string) ([]ServerMetadata, error) { } // ListServers returns all servers (both container and remote) -func (p *BaseProvider) ListServers() ([]ServerMetadata, error) { +func (p *BaseProvider) ListServers() ([]types.ServerMetadata, error) { reg, err := p.GetRegistryFunc() if err != nil { return nil, err @@ -76,14 +78,14 @@ func (p *BaseProvider) ListServers() ([]ServerMetadata, error) { // Legacy methods for backward compatibility // GetImageServer returns a specific container server by name (legacy method) -func (p *BaseProvider) GetImageServer(name string) (*ImageMetadata, error) { +func (p *BaseProvider) GetImageServer(name string) (*types.ImageMetadata, error) { server, err := p.GetServer(name) if err != nil { return nil, err } // Type assert to ImageMetadata - if img, ok := server.(*ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { return img, nil } @@ -91,16 +93,16 @@ func (p *BaseProvider) GetImageServer(name string) (*ImageMetadata, error) { } // SearchImageServers searches for container servers matching the query (legacy method) -func (p *BaseProvider) SearchImageServers(query string) ([]*ImageMetadata, error) { +func (p *BaseProvider) SearchImageServers(query string) ([]*types.ImageMetadata, error) { servers, err := p.SearchServers(query) if err != nil { return nil, err } // Filter to only container servers - var results []*ImageMetadata + var results []*types.ImageMetadata for _, server := range servers { - if img, ok := server.(*ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { results = append(results, img) } } @@ -109,16 +111,16 @@ func (p *BaseProvider) SearchImageServers(query string) ([]*ImageMetadata, error } // ListImageServers returns all container servers (legacy method) -func (p *BaseProvider) ListImageServers() ([]*ImageMetadata, error) { +func (p *BaseProvider) ListImageServers() ([]*types.ImageMetadata, error) { servers, err := p.ListServers() if err != nil { return nil, err } // Filter to only container servers - var results []*ImageMetadata + var results []*types.ImageMetadata for _, server := range servers { - if img, ok := server.(*ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { results = append(results, img) } } diff --git a/pkg/registry/provider_local.go b/pkg/registry/provider_local.go index 09dfa0cd5..2848e978d 100644 --- a/pkg/registry/provider_local.go +++ b/pkg/registry/provider_local.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "os" + + "github.com/stacklok/toolhive/pkg/registry/types" ) //go:embed data/registry.json @@ -35,7 +37,7 @@ func NewLocalRegistryProvider(filePath ...string) *LocalRegistryProvider { } // GetRegistry returns the registry data from file path or embedded data -func (p *LocalRegistryProvider) GetRegistry() (*Registry, error) { +func (p *LocalRegistryProvider) GetRegistry() (*types.Registry, error) { var data []byte var err error @@ -88,8 +90,8 @@ func (p *LocalRegistryProvider) GetRegistry() (*Registry, error) { } // parseRegistryData parses JSON data into a Registry struct -func parseRegistryData(data []byte) (*Registry, error) { - registry := &Registry{} +func parseRegistryData(data []byte) (*types.Registry, error) { + registry := &types.Registry{} if err := json.Unmarshal(data, registry); err != nil { return nil, fmt.Errorf("failed to parse registry data: %w", err) } diff --git a/pkg/registry/provider_remote.go b/pkg/registry/provider_remote.go index 2349e42a1..3d19a074d 100644 --- a/pkg/registry/provider_remote.go +++ b/pkg/registry/provider_remote.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/stacklok/toolhive/pkg/networking" + "github.com/stacklok/toolhive/pkg/registry/types" ) // RemoteRegistryProvider provides registry data from a remote HTTP endpoint @@ -30,7 +31,7 @@ func NewRemoteRegistryProvider(registryURL string, allowPrivateIp bool) *RemoteR } // GetRegistry returns the remote registry data -func (p *RemoteRegistryProvider) GetRegistry() (*Registry, error) { +func (p *RemoteRegistryProvider) GetRegistry() (*types.Registry, error) { client, err := networking.NewHttpClientBuilder(). WithPrivateIPs(p.allowPrivateIp). Build() @@ -55,7 +56,7 @@ func (p *RemoteRegistryProvider) GetRegistry() (*Registry, error) { return nil, fmt.Errorf("failed to read registry data from response body: %w", err) } - registry := &Registry{} + registry := &types.Registry{} if err := json.Unmarshal(data, registry); err != nil { return nil, fmt.Errorf("failed to parse registry data: %w", err) } diff --git a/pkg/registry/types.go b/pkg/registry/types/registry_types.go similarity index 99% rename from pkg/registry/types.go rename to pkg/registry/types/registry_types.go index b29045f85..bf7d5fd86 100644 --- a/pkg/registry/types.go +++ b/pkg/registry/types/registry_types.go @@ -1,5 +1,5 @@ -// Package registry provides access to the MCP server registry -package registry +// Package types contains the core type definitions for the MCP registry system. +package types import ( "sort" diff --git a/pkg/registry/upstream_conversion.go b/pkg/registry/upstream_conversion.go index c87291717..166b478e7 100644 --- a/pkg/registry/upstream_conversion.go +++ b/pkg/registry/upstream_conversion.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stacklok/toolhive/pkg/permissions" + "github.com/stacklok/toolhive/pkg/registry/types" ) // ToolhiveExtensionKey is the key used for ToolHive-specific metadata in the x-publisher field @@ -20,7 +21,7 @@ type ToolhivePublisherExtension struct { // Tools is a list of tool names provided by this MCP server Tools []string `json:"tools,omitempty" yaml:"tools,omitempty"` // Metadata contains additional information about the server - Metadata *Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Metadata *types.Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` // Tags are categorization labels for the server Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` // CustomMetadata allows for additional user-defined metadata @@ -32,11 +33,11 @@ type ToolhivePublisherExtension struct { // DockerTags lists the available Docker tags for this server image DockerTags []string `json:"docker_tags,omitempty" yaml:"docker_tags,omitempty"` // Provenance contains verification and signing metadata - Provenance *Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"` + Provenance *types.Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"` } // ConvertUpstreamToToolhive converts an upstream server detail to toolhive format -func ConvertUpstreamToToolhive(upstream *UpstreamServerDetail) (ServerMetadata, error) { +func ConvertUpstreamToToolhive(upstream *UpstreamServerDetail) (types.ServerMetadata, error) { if upstream == nil { return nil, fmt.Errorf("upstream server detail is nil") } @@ -56,9 +57,9 @@ func ConvertUpstreamToToolhive(upstream *UpstreamServerDetail) (ServerMetadata, } // convertToRemoteServer converts upstream format to RemoteServerMetadata -func convertToRemoteServer(upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension) *RemoteServerMetadata { - remote := &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ +func convertToRemoteServer(upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension) *types.RemoteServerMetadata { + remote := &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: upstream.Server.Name, Description: upstream.Server.Description, Tier: getStringOrDefault(toolhiveExt, func(t *ToolhivePublisherExtension) string { return t.Tier }, "Community"), @@ -85,7 +86,7 @@ func convertToRemoteServer(upstream *UpstreamServerDetail, toolhiveExt *Toolhive } // convertToImageMetadata converts upstream format to ImageMetadata -func convertToImageMetadata(upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension) (*ImageMetadata, error) { +func convertToImageMetadata(upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension) (*types.ImageMetadata, error) { if len(upstream.Server.Packages) == 0 { return nil, fmt.Errorf("no packages found for container server") } @@ -103,8 +104,8 @@ func convertToImageMetadata(upstream *UpstreamServerDetail, toolhiveExt *Toolhiv } } - image := &ImageMetadata{ - BaseServerMetadata: BaseServerMetadata{ + image := &types.ImageMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: upstream.Server.Name, Description: upstream.Server.Description, Tier: getStringOrDefault(toolhiveExt, func(t *ToolhivePublisherExtension) string { return t.Tier }, "Community"), @@ -130,7 +131,7 @@ func convertToImageMetadata(upstream *UpstreamServerDetail, toolhiveExt *Toolhiv } // ConvertToolhiveToUpstream converts toolhive format to upstream format -func ConvertToolhiveToUpstream(server ServerMetadata) (*UpstreamServerDetail, error) { +func ConvertToolhiveToUpstream(server types.ServerMetadata) (*UpstreamServerDetail, error) { if server == nil { return nil, fmt.Errorf("server metadata is nil") } @@ -165,7 +166,7 @@ func ConvertToolhiveToUpstream(server ServerMetadata) (*UpstreamServerDetail, er } if server.IsRemote() { - if remoteServer, ok := server.(*RemoteServerMetadata); ok { + if remoteServer, ok := server.(*types.RemoteServerMetadata); ok { upstream.Server.Remotes = []UpstreamRemote{ { TransportType: convertTransportToUpstream(remoteServer.Transport), @@ -176,7 +177,7 @@ func ConvertToolhiveToUpstream(server ServerMetadata) (*UpstreamServerDetail, er upstream.Server.Packages = convertEnvVarsToPackages(remoteServer.EnvVars) } } else { - if imageServer, ok := server.(*ImageMetadata); ok { + if imageServer, ok := server.(*types.ImageMetadata); ok { toolhiveExt.TargetPort = imageServer.TargetPort toolhiveExt.Permissions = imageServer.Permissions toolhiveExt.DockerTags = imageServer.DockerTags @@ -246,10 +247,10 @@ func convertTransportToUpstream(transport string) UpstreamTransportType { } } -func convertHeaders(headers []UpstreamKeyValueInput) []*Header { - result := make([]*Header, 0, len(headers)) +func convertHeaders(headers []UpstreamKeyValueInput) []*types.Header { + result := make([]*types.Header, 0, len(headers)) for _, h := range headers { - result = append(result, &Header{ + result = append(result, &types.Header{ Name: h.Name, Description: h.Description, Required: h.IsRequired, @@ -261,7 +262,7 @@ func convertHeaders(headers []UpstreamKeyValueInput) []*Header { return result } -func convertHeadersToUpstream(headers []*Header) []UpstreamKeyValueInput { +func convertHeadersToUpstream(headers []*types.Header) []UpstreamKeyValueInput { result := make([]UpstreamKeyValueInput, 0, len(headers)) for _, h := range headers { result = append(result, UpstreamKeyValueInput{ @@ -276,10 +277,10 @@ func convertHeadersToUpstream(headers []*Header) []UpstreamKeyValueInput { return result } -func convertEnvironmentVariables(envVars []UpstreamKeyValueInput) []*EnvVar { - result := make([]*EnvVar, 0, len(envVars)) +func convertEnvironmentVariables(envVars []UpstreamKeyValueInput) []*types.EnvVar { + result := make([]*types.EnvVar, 0, len(envVars)) for _, ev := range envVars { - result = append(result, &EnvVar{ + result = append(result, &types.EnvVar{ Name: ev.Name, Description: ev.Description, Required: ev.IsRequired, @@ -290,7 +291,7 @@ func convertEnvironmentVariables(envVars []UpstreamKeyValueInput) []*EnvVar { return result } -func convertEnvVarsToUpstream(envVars []*EnvVar) []UpstreamKeyValueInput { +func convertEnvVarsToUpstream(envVars []*types.EnvVar) []UpstreamKeyValueInput { result := make([]UpstreamKeyValueInput, 0, len(envVars)) for _, ev := range envVars { result = append(result, UpstreamKeyValueInput{ @@ -392,7 +393,7 @@ func getPackageEnvVars(packages []UpstreamPackage) []UpstreamKeyValueInput { return nil } -func convertEnvVarsToPackages(envVars []*EnvVar) []UpstreamPackage { +func convertEnvVarsToPackages(envVars []*types.EnvVar) []UpstreamPackage { if len(envVars) == 0 { return nil } @@ -446,11 +447,11 @@ func getIntOrDefault[T any](ext *T, getter func(*T) int, defaultValue int) int { return defaultValue } -func getMetadataOrDefault(ext *ToolhivePublisherExtension) *Metadata { +func getMetadataOrDefault(ext *ToolhivePublisherExtension) *types.Metadata { if ext != nil && ext.Metadata != nil { return ext.Metadata } - return &Metadata{ + return &types.Metadata{ Stars: 0, Pulls: 0, LastUpdated: time.Now().Format(time.RFC3339), @@ -464,7 +465,7 @@ func getPermissionsOrDefault(ext *ToolhivePublisherExtension) *permissions.Profi return nil } -func getProvenanceOrDefault(ext *ToolhivePublisherExtension) *Provenance { +func getProvenanceOrDefault(ext *ToolhivePublisherExtension) *types.Provenance { if ext != nil && ext.Provenance != nil { return ext.Provenance } diff --git a/pkg/runner/config.go b/pkg/runner/config.go index f4897c233..3716b48a7 100644 --- a/pkg/runner/config.go +++ b/pkg/runner/config.go @@ -20,6 +20,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/permissions" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/secrets" "github.com/stacklok/toolhive/pkg/state" "github.com/stacklok/toolhive/pkg/telemetry" diff --git a/pkg/runner/config_builder.go b/pkg/runner/config_builder.go index 2e0807084..eb6c3f2bd 100644 --- a/pkg/runner/config_builder.go +++ b/pkg/runner/config_builder.go @@ -18,7 +18,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/mcp" "github.com/stacklok/toolhive/pkg/permissions" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/telemetry" "github.com/stacklok/toolhive/pkg/transport" "github.com/stacklok/toolhive/pkg/transport/types" @@ -661,7 +661,7 @@ func addAuditMiddleware( // NewOperatorRunConfigBuilder creates a new RunConfigBuilder configured for operator use func NewOperatorRunConfigBuilder( ctx context.Context, - imageMetadata *registry.ImageMetadata, + imageMetadata *regtypes.ImageMetadata, envVars map[string]string, envVarValidator EnvVarValidator, runConfigOptions ...RunConfigBuilderOption, @@ -679,7 +679,7 @@ func NewOperatorRunConfigBuilder( // NewRunConfigBuilder creates the final RunConfig instance with validation and processing func NewRunConfigBuilder( ctx context.Context, - imageMetadata *registry.ImageMetadata, + imageMetadata *regtypes.ImageMetadata, envVars map[string]string, envVarValidator EnvVarValidator, runConfigOptions ...RunConfigBuilderOption, @@ -697,7 +697,7 @@ func NewRunConfigBuilder( func internalRunConfigBuilder( ctx context.Context, b *runConfigBuilder, - imageMetadata *registry.ImageMetadata, + imageMetadata *regtypes.ImageMetadata, envVars map[string]string, envVarValidator EnvVarValidator, runConfigOptions ...RunConfigBuilderOption, @@ -745,7 +745,7 @@ func internalRunConfigBuilder( // This function also handles setting missing values based on the image metadata (if present). // //nolint:gocyclo // This function needs to be refactored to reduce cyclomatic complexity. -func (b *runConfigBuilder) validateConfig(imageMetadata *registry.ImageMetadata) error { +func (b *runConfigBuilder) validateConfig(imageMetadata *regtypes.ImageMetadata) error { c := b.config var err error @@ -886,7 +886,7 @@ func (b *runConfigBuilder) validateConfig(imageMetadata *registry.ImageMetadata) return nil } -func (b *runConfigBuilder) loadPermissionProfile(imageMetadata *registry.ImageMetadata) (*permissions.Profile, error) { +func (b *runConfigBuilder) loadPermissionProfile(imageMetadata *regtypes.ImageMetadata) (*permissions.Profile, error) { // The permission profile object takes precedence over the name or path. if b.config.PermissionProfile != nil { return b.config.PermissionProfile, nil diff --git a/pkg/runner/config_builder_test.go b/pkg/runner/config_builder_test.go index 6acd10f63..8a066b916 100644 --- a/pkg/runner/config_builder_test.go +++ b/pkg/runner/config_builder_test.go @@ -15,6 +15,7 @@ import ( "github.com/stacklok/toolhive/pkg/mcp" "github.com/stacklok/toolhive/pkg/permissions" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -33,7 +34,7 @@ func TestRunConfigBuilder_Build_WithPermissionProfile(t *testing.T) { "network": "invalid-network-format" }` - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", }, @@ -69,7 +70,7 @@ func TestRunConfigBuilder_Build_WithPermissionProfile(t *testing.T) { profileContent string // The JSON content for the profile file needsTempFile bool // Whether this test case needs a temp file expectedPermissionProfile *permissions.Profile - imageMetadata *registry.ImageMetadata + imageMetadata *regtypes.ImageMetadata expectError bool }{ { @@ -555,7 +556,7 @@ func TestRunConfigBuilder_ToolOverrideMutualExclusivity(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, @@ -634,7 +635,7 @@ func TestRunConfigBuilder_ToolOverrideWithToolsFilter(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, @@ -698,7 +699,7 @@ func TestNewOperatorRunConfigBuilder(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, @@ -767,7 +768,7 @@ func TestWithEnvVars(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, @@ -795,7 +796,7 @@ func TestWithEnvVarsOverwrite(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, @@ -880,7 +881,7 @@ func TestBuildForOperator(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, diff --git a/pkg/runner/config_test.go b/pkg/runner/config_test.go index f7153bd61..8361d9015 100644 --- a/pkg/runner/config_test.go +++ b/pkg/runner/config_test.go @@ -18,6 +18,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/permissions" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" secretsmocks "github.com/stacklok/toolhive/pkg/secrets/mocks" "github.com/stacklok/toolhive/pkg/telemetry" "github.com/stacklok/toolhive/pkg/transport/types" @@ -551,7 +552,7 @@ func TestRunConfig_WithAuthz(t *testing.T) { // mockEnvVarValidator implements the EnvVarValidator interface for testing type mockEnvVarValidator struct{} -func (*mockEnvVarValidator) Validate(_ context.Context, _ *registry.ImageMetadata, _ *RunConfig, suppliedEnvVars map[string]string) (map[string]string, error) { +func (*mockEnvVarValidator) Validate(_ context.Context, _ *regtypes.ImageMetadata, _ *RunConfig, suppliedEnvVars map[string]string) (map[string]string, error) { // For testing, just return the supplied environment variables as-is return suppliedEnvVars, nil } @@ -565,7 +566,7 @@ func TestRunConfigBuilder(t *testing.T) { cmdArgs := []string{"arg1", "arg2"} name := "test-server" imageURL := "test-image:latest" - imageMetadata := ®istry.ImageMetadata{ + imageMetadata := ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Name: "test-metadata-name", Transport: "sse", @@ -780,7 +781,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { name string userTransport string userTargetPort int - metadata *registry.ImageMetadata + metadata *regtypes.ImageMetadata expectedTransport types.TransportType expectedTargetPort int }{ @@ -788,7 +789,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { name: "Metadata transport used when user doesn't specify", userTransport: "", userTargetPort: 0, - metadata: ®istry.ImageMetadata{ + metadata: ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "streamable-http", }, @@ -801,7 +802,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { name: "User transport overrides metadata", userTransport: "stdio", userTargetPort: 0, - metadata: ®istry.ImageMetadata{ + metadata: ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "sse", }, @@ -814,7 +815,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { name: "User target port overrides metadata", userTransport: "sse", userTargetPort: 4000, - metadata: ®istry.ImageMetadata{ + metadata: ®types.ImageMetadata{ BaseServerMetadata: registry.BaseServerMetadata{ Transport: "sse", }, @@ -936,7 +937,7 @@ func TestRunConfigBuilder_CmdArgsMetadataOverride(t *testing.T) { validator := &mockEnvVarValidator{} userArgs := []string{"--user-arg1", "--user-arg2"} - metadata := ®istry.ImageMetadata{ + metadata := ®types.ImageMetadata{ Args: []string{"--metadata-arg1", "--metadata-arg2"}, } @@ -991,7 +992,7 @@ func TestRunConfigBuilder_CmdArgsMetadataDefaults(t *testing.T) { // No user args provided userArgs := []string{} - metadata := ®istry.ImageMetadata{ + metadata := ®types.ImageMetadata{ Args: []string{"--metadata-arg1", "--metadata-arg2"}, } @@ -1112,7 +1113,7 @@ func TestRunConfigBuilder_FilesystemMCPScenario(t *testing.T) { validator := &mockEnvVarValidator{} // Simulate the filesystem MCP registry configuration - metadata := ®istry.ImageMetadata{ + metadata := ®types.ImageMetadata{ Args: []string{"/projects"}, // Default args from registry } diff --git a/pkg/runner/env.go b/pkg/runner/env.go index 3cbf66ba6..6f0d80d36 100644 --- a/pkg/runner/env.go +++ b/pkg/runner/env.go @@ -10,7 +10,7 @@ import ( "github.com/stacklok/toolhive/pkg/config" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/secrets" ) @@ -23,7 +23,7 @@ type EnvVarValidator interface { // and returns the processed environment variables to be set. Validate( ctx context.Context, - metadata *registry.ImageMetadata, + metadata *regtypes.ImageMetadata, runConfig *RunConfig, suppliedEnvVars map[string]string, ) (map[string]string, error) @@ -38,7 +38,7 @@ type DetachedEnvVarValidator struct{} // and returns the processed environment variables to be set. func (*DetachedEnvVarValidator) Validate( _ context.Context, - metadata *registry.ImageMetadata, + metadata *regtypes.ImageMetadata, runConfig *RunConfig, suppliedEnvVars map[string]string, ) (map[string]string, error) { @@ -80,7 +80,7 @@ func NewCLIEnvVarValidator(configProvider config.Provider) *CLIEnvVarValidator { // and returns the processed environment variables to be set. func (v *CLIEnvVarValidator) Validate( ctx context.Context, - metadata *registry.ImageMetadata, + metadata *regtypes.ImageMetadata, runConfig *RunConfig, suppliedEnvVars map[string]string, ) (map[string]string, error) { @@ -152,7 +152,7 @@ func (v *CLIEnvVarValidator) Validate( } // promptForEnvironmentVariable prompts the user for an environment variable value -func promptForEnvironmentVariable(envVar *registry.EnvVar) (string, error) { +func promptForEnvironmentVariable(envVar *regtypes.EnvVar) (string, error) { var byteValue []byte var err error if envVar.Secret { @@ -182,7 +182,7 @@ func promptForEnvironmentVariable(envVar *registry.EnvVar) (string, error) { // addNewVariable adds an environment variable or secret to the appropriate list func addNewVariable( ctx context.Context, - envVar *registry.EnvVar, + envVar *regtypes.EnvVar, value string, secretsManager secrets.Provider, envVars *map[string]string, @@ -198,7 +198,7 @@ func addNewVariable( // addAsSecret stores the value as a secret and adds a secret reference func addAsSecret( ctx context.Context, - envVar *registry.EnvVar, + envVar *regtypes.EnvVar, value string, secretsManager secrets.Provider, secretsList *[]string, @@ -229,7 +229,7 @@ func addAsSecret( } // initializeSecretsManagerIfNeeded initializes the secrets manager if there are secret environment variables -func (v *CLIEnvVarValidator) initializeSecretsManagerIfNeeded(registryEnvVars []*registry.EnvVar) secrets.Provider { +func (v *CLIEnvVarValidator) initializeSecretsManagerIfNeeded(registryEnvVars []*regtypes.EnvVar) secrets.Provider { // Check if we have any secret environment variables hasSecrets := false for _, envVar := range registryEnvVars { @@ -319,7 +319,7 @@ func isSecretReferenceEnvVar(secret, envVarName string) bool { // addAsEnvironmentVariable adds the value as a regular environment variable func addAsEnvironmentVariable( - envVar *registry.EnvVar, + envVar *regtypes.EnvVar, value string, envVars *map[string]string, ) { diff --git a/pkg/runner/retriever/retriever.go b/pkg/runner/retriever/retriever.go index 689686367..598873bcc 100644 --- a/pkg/runner/retriever/retriever.go +++ b/pkg/runner/retriever/retriever.go @@ -13,6 +13,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/verifier" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" ) @@ -35,7 +36,7 @@ var ( ) // Retriever is a function that retrieves the MCP server definition from the registry. -type Retriever func(context.Context, string, string, string, string) (string, registry.ServerMetadata, error) +type Retriever func(context.Context, string, string, string, string) (string, types.ServerMetadata, error) // GetMCPServer retrieves the MCP server definition from the registry. func GetMCPServer( @@ -44,8 +45,8 @@ func GetMCPServer( rawCACertPath string, verificationType string, groupName string, -) (string, registry.ServerMetadata, error) { - var imageMetadata *registry.ImageMetadata +) (string, types.ServerMetadata, error) { + var imageMetadata *types.ImageMetadata var imageToUse string imageManager := images.NewImageManager(ctx) @@ -62,7 +63,7 @@ func GetMCPServer( // If group name is provided, look up server in the group first if groupName != "" { var err error - var server registry.ServerMetadata + var server types.ServerMetadata imageToUse, imageMetadata, server, err = handleGroupLookup(ctx, serverOrImage, groupName) if err != nil { return "", nil, err @@ -73,7 +74,7 @@ func GetMCPServer( } } else { var err error - var server registry.ServerMetadata + var server types.ServerMetadata imageToUse, imageMetadata, server, err = handleRegistryLookup(ctx, serverOrImage) if err != nil { return "", nil, err @@ -111,8 +112,8 @@ func handleProtocolScheme( serverOrImage string, rawCACertPath string, imageManager images.ImageManager, -) (string, *registry.ImageMetadata, error) { - var imageMetadata *registry.ImageMetadata +) (string, *types.ImageMetadata, error) { + var imageMetadata *types.ImageMetadata var imageToUse string logger.Debugf("Detected protocol scheme: %s", serverOrImage) @@ -134,8 +135,8 @@ func handleGroupLookup( _ context.Context, serverOrImage string, groupName string, -) (string, *registry.ImageMetadata, registry.ServerMetadata, error) { - var imageMetadata *registry.ImageMetadata +) (string, *types.ImageMetadata, types.ServerMetadata, error) { + var imageMetadata *types.ImageMetadata var imageToUse string provider, err := registry.GetDefaultProvider() @@ -154,7 +155,7 @@ func handleGroupLookup( } // First check if the server exists and whether it's remote - var server registry.ServerMetadata + var server types.ServerMetadata var serverFound bool if containerServer, exists := group.Servers[serverOrImage]; exists { server = containerServer @@ -170,7 +171,7 @@ func handleGroupLookup( return serverOrImage, nil, server, nil } // It's a container server, get the ImageMetadata - if imgMetadata, ok := server.(*registry.ImageMetadata); ok { + if imgMetadata, ok := server.(*types.ImageMetadata); ok { imageMetadata = imgMetadata logger.Debugf("Found imageMetadata '%s' in group: %v", serverOrImage, imageMetadata) imageToUse = imageMetadata.Image @@ -191,8 +192,8 @@ func handleGroupLookup( func handleRegistryLookup( _ context.Context, serverOrImage string, -) (string, *registry.ImageMetadata, registry.ServerMetadata, error) { - var imageMetadata *registry.ImageMetadata +) (string, *types.ImageMetadata, types.ServerMetadata, error) { + var imageMetadata *types.ImageMetadata var imageToUse string // Try to find the server in the registry @@ -316,7 +317,7 @@ func resolveCACertPath(flagValue string) string { } // verifyImage verifies the image using the specified verification setting (warn, enabled, or disabled) -func verifyImage(image string, server *registry.ImageMetadata, verifySetting string) error { +func verifyImage(image string, server *types.ImageMetadata, verifySetting string) error { switch verifySetting { case VerifyImageDisabled: logger.Warn("Image verification is disabled") From 66cb469dc1859a1392966273764a70825f79dc08 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 13:02:22 +0200 Subject: [PATCH 06/20] Fix the imports in the tests Signed-off-by: Radoslav Dimitrov --- .../converters/converters_fixture_test.go | 14 ++--- pkg/registry/converters/converters_test.go | 30 ++++----- pkg/registry/converters/integration_test.go | 22 +++---- pkg/registry/provider_test.go | 4 +- pkg/registry/schema_validation_test.go | 4 +- pkg/registry/types_test.go | 62 ++++++++++--------- pkg/registry/upstream_conversion_test.go | 16 ++--- 7 files changed, 80 insertions(+), 72 deletions(-) diff --git a/pkg/registry/converters/converters_fixture_test.go b/pkg/registry/converters/converters_fixture_test.go index 2c15875eb..0ccf5c4db 100644 --- a/pkg/registry/converters/converters_fixture_test.go +++ b/pkg/registry/converters/converters_fixture_test.go @@ -7,7 +7,7 @@ import ( "testing" upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -113,7 +113,7 @@ func TestConverters_Fixtures(t *testing.T) { func convertImageToServer(t *testing.T, inputData []byte, serverName string) []byte { t.Helper() - var imageMetadata registry.ImageMetadata + var imageMetadata types.ImageMetadata require.NoError(t, json.Unmarshal(inputData, &imageMetadata)) serverJSON, err := ImageMetadataToServerJSON(serverName, &imageMetadata) @@ -139,7 +139,7 @@ func convertServerToImage(t *testing.T, inputData []byte) []byte { func convertRemoteToServer(t *testing.T, inputData []byte, serverName string) []byte { t.Helper() - var remoteMetadata registry.RemoteServerMetadata + var remoteMetadata types.RemoteServerMetadata require.NoError(t, json.Unmarshal(inputData, &remoteMetadata)) serverJSON, err := RemoteServerMetadataToServerJSON(serverName, &remoteMetadata) @@ -167,7 +167,7 @@ func convertServerToRemote(t *testing.T, inputData []byte) []byte { func validateImageToServerConversion(t *testing.T, inputData, outputData []byte) { t.Helper() - var input registry.ImageMetadata + var input types.ImageMetadata var output upstream.ServerJSON require.NoError(t, json.Unmarshal(inputData, &input)) @@ -203,7 +203,7 @@ func validateImageToServerConversion(t *testing.T, inputData, outputData []byte) func validateServerToImageConversion(t *testing.T, inputData, outputData []byte) { t.Helper() var input upstream.ServerJSON - var output registry.ImageMetadata + var output types.ImageMetadata require.NoError(t, json.Unmarshal(inputData, &input)) require.NoError(t, json.Unmarshal(outputData, &output)) @@ -221,7 +221,7 @@ func validateServerToImageConversion(t *testing.T, inputData, outputData []byte) func validateRemoteToServerConversion(t *testing.T, inputData, outputData []byte) { t.Helper() - var input registry.RemoteServerMetadata + var input types.RemoteServerMetadata var output upstream.ServerJSON require.NoError(t, json.Unmarshal(inputData, &input)) @@ -241,7 +241,7 @@ func validateRemoteToServerConversion(t *testing.T, inputData, outputData []byte func validateServerToRemoteConversion(t *testing.T, inputData, outputData []byte) { t.Helper() var input upstream.ServerJSON - var output registry.RemoteServerMetadata + var output types.RemoteServerMetadata require.NoError(t, json.Unmarshal(inputData, &input)) require.NoError(t, json.Unmarshal(outputData, &output)) diff --git a/pkg/registry/converters/converters_test.go b/pkg/registry/converters/converters_test.go index 65c580f46..dfa66932c 100644 --- a/pkg/registry/converters/converters_test.go +++ b/pkg/registry/converters/converters_test.go @@ -6,7 +6,7 @@ import ( upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -54,9 +54,9 @@ func createTestServerJSON() *upstream.ServerJSON { } // createTestImageMetadata creates a valid ImageMetadata for testing -func createTestImageMetadata() *registry.ImageMetadata { - return ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func createTestImageMetadata() *types.ImageMetadata { + return &types.ImageMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Description: "Test MCP server", Transport: model.TransportTypeStdio, RepositoryURL: "https://github.com/test/repo", @@ -64,7 +64,7 @@ func createTestImageMetadata() *registry.ImageMetadata { Tier: "Official", Tools: []string{"tool1", "tool2"}, Tags: []string{"test", "example"}, - Metadata: ®istry.Metadata{ + Metadata: &types.Metadata{ Stars: 100, Pulls: 1000, LastUpdated: "2025-01-01", @@ -75,9 +75,9 @@ func createTestImageMetadata() *registry.ImageMetadata { } // createTestRemoteServerMetadata creates a valid RemoteServerMetadata for testing -func createTestRemoteServerMetadata() *registry.RemoteServerMetadata { - return ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func createTestRemoteServerMetadata() *types.RemoteServerMetadata { + return &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Description: "Test remote server", Transport: "sse", RepositoryURL: "https://github.com/test/remote", @@ -329,7 +329,7 @@ func TestImageMetadataToServerJSON_WithEnvVars(t *testing.T) { t.Parallel() imageMetadata := createTestImageMetadata() - imageMetadata.EnvVars = []*registry.EnvVar{ + imageMetadata.EnvVars = []*types.EnvVar{ { Name: "API_KEY", Description: "API Key", @@ -626,7 +626,7 @@ func TestRemoteServerMetadataToServerJSON_WithHeaders(t *testing.T) { t.Parallel() remoteMetadata := createTestRemoteServerMetadata() - remoteMetadata.Headers = []*registry.Header{ + remoteMetadata.Headers = []*types.Header{ { Name: "Authorization", Description: "Auth header", @@ -803,8 +803,8 @@ func TestRoundTrip_ImageMetadataWithAllFields(t *testing.T) { t.Parallel() // Create ImageMetadata with maximum field population - original := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + original := &types.ImageMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Description: "Full featured server", Transport: model.TransportTypeStreamableHTTP, RepositoryURL: "https://github.com/test/full", @@ -812,7 +812,7 @@ func TestRoundTrip_ImageMetadataWithAllFields(t *testing.T) { Tier: "Official", Tools: []string{"tool1", "tool2", "tool3"}, Tags: []string{"tag1", "tag2"}, - Metadata: ®istry.Metadata{ + Metadata: &types.Metadata{ Stars: 500, Pulls: 10000, LastUpdated: "2025-10-23", @@ -820,7 +820,7 @@ func TestRoundTrip_ImageMetadataWithAllFields(t *testing.T) { }, Image: "ghcr.io/test/full:v1.0.0", TargetPort: 8080, - EnvVars: []*registry.EnvVar{ + EnvVars: []*types.EnvVar{ { Name: "API_KEY", Description: "API Key for authentication", @@ -1154,7 +1154,7 @@ func TestRealWorld_GitHubServer_ExactData(t *testing.T) { }` // Parse ImageMetadata JSON - var imageMetadata registry.ImageMetadata + var imageMetadata types.ImageMetadata err := json.Unmarshal([]byte(imageMetadataJSON), &imageMetadata) require.NoError(t, err, "Should parse ImageMetadata JSON") diff --git a/pkg/registry/converters/integration_test.go b/pkg/registry/converters/integration_test.go index 02f19f7e4..aab5328a9 100644 --- a/pkg/registry/converters/integration_test.go +++ b/pkg/registry/converters/integration_test.go @@ -9,7 +9,7 @@ import ( "testing" upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/require" ) @@ -20,7 +20,7 @@ type ToolHiveRegistry struct { } // parseServerEntry parses a server entry as either ImageMetadata or RemoteServerMetadata -func parseServerEntry(data json.RawMessage) (imageMetadata *registry.ImageMetadata, remoteMetadata *registry.RemoteServerMetadata, err error) { +func parseServerEntry(data json.RawMessage) (imageMetadata *types.ImageMetadata, remoteMetadata *types.RemoteServerMetadata, err error) { // Try to detect type by checking for "image" vs "url" field var typeCheck map[string]interface{} if err := json.Unmarshal(data, &typeCheck); err != nil { @@ -29,14 +29,14 @@ func parseServerEntry(data json.RawMessage) (imageMetadata *registry.ImageMetada if _, hasImage := typeCheck["image"]; hasImage { // It's an ImageMetadata - var img registry.ImageMetadata + var img types.ImageMetadata if err := json.Unmarshal(data, &img); err != nil { return nil, nil, err } return &img, nil, nil } else if _, hasURL := typeCheck["url"]; hasURL { // It's a RemoteServerMetadata - var remote registry.RemoteServerMetadata + var remote types.RemoteServerMetadata if err := json.Unmarshal(data, &remote); err != nil { return nil, nil, err } @@ -169,7 +169,7 @@ func TestRoundTrip_RealRegistryData(t *testing.T) { t.Logf("%s", separator) } -func testImageServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *registry.ImageMetadata, stats *struct { +func testImageServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *types.ImageMetadata, stats *struct { total int imageServers int remoteServers int @@ -194,7 +194,7 @@ func testImageServerRoundTrip(t *testing.T, name string, serverJSON *upstream.Se compareImageMetadata(t, name, original, converted, stats) } -func testRemoteServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *registry.RemoteServerMetadata, stats *struct { +func testRemoteServerRoundTrip(t *testing.T, name string, serverJSON *upstream.ServerJSON, original *types.RemoteServerMetadata, stats *struct { total int imageServers int remoteServers int @@ -219,7 +219,7 @@ func testRemoteServerRoundTrip(t *testing.T, name string, serverJSON *upstream.S compareRemoteServerMetadata(t, name, original, converted, stats) } -func compareImageMetadata(t *testing.T, name string, original, converted *registry.ImageMetadata, stats *struct { +func compareImageMetadata(t *testing.T, name string, original, converted *types.ImageMetadata, stats *struct { total int imageServers int remoteServers int @@ -287,7 +287,7 @@ func compareImageMetadata(t *testing.T, name string, original, converted *regist } } -func compareRemoteServerMetadata(t *testing.T, name string, original, converted *registry.RemoteServerMetadata, stats *struct { +func compareRemoteServerMetadata(t *testing.T, name string, original, converted *types.RemoteServerMetadata, stats *struct { total int imageServers int remoteServers int @@ -372,7 +372,7 @@ func stringSlicesEqual(a, b []string) bool { return true } -func envVarsEqual(a, b *registry.EnvVar) bool { +func envVarsEqual(a, b *types.EnvVar) bool { if a == nil && b == nil { return true } @@ -386,7 +386,7 @@ func envVarsEqual(a, b *registry.EnvVar) bool { a.Default == b.Default } -func headersEqual(a, b *registry.Header) bool { +func headersEqual(a, b *types.Header) bool { if a == nil && b == nil { return true } @@ -399,7 +399,7 @@ func headersEqual(a, b *registry.Header) bool { a.Secret == b.Secret } -func metadataEqual(a, b *registry.Metadata) bool { +func metadataEqual(a, b *types.Metadata) bool { if a == nil && b == nil { return true } diff --git a/pkg/registry/provider_test.go b/pkg/registry/provider_test.go index 1f3cd222a..f95779917 100644 --- a/pkg/registry/provider_test.go +++ b/pkg/registry/provider_test.go @@ -1,6 +1,8 @@ package registry import ( + + "github.com/stacklok/toolhive/pkg/registry/types" "os" "path/filepath" "testing" @@ -333,7 +335,7 @@ func TestGetServer(t *testing.T) { // Check if it's a container server and has an image if !server.IsRemote() { - if img, ok := server.(*ImageMetadata); ok { + if img, ok := server.(*types.ImageMetadata); ok { if img.Image == "" { t.Error("ImageMetadata image is empty") } diff --git a/pkg/registry/schema_validation_test.go b/pkg/registry/schema_validation_test.go index 155ce0de4..9b3636190 100644 --- a/pkg/registry/schema_validation_test.go +++ b/pkg/registry/schema_validation_test.go @@ -1,6 +1,8 @@ package registry import ( + + "github.com/stacklok/toolhive/pkg/registry/types" "encoding/json" "testing" @@ -341,7 +343,7 @@ func TestValidateEmbeddedRegistryCanLoadData(t *testing.T) { require.NoError(t, err, "Should be able to load embedded registry data") // Verify it's valid JSON - var registry Registry + var registry types.Registry err = json.Unmarshal(registryData, ®istry) require.NoError(t, err, "Embedded registry should be valid JSON") diff --git a/pkg/registry/types_test.go b/pkg/registry/types_test.go index 2520aae6a..0430da5de 100644 --- a/pkg/registry/types_test.go +++ b/pkg/registry/types_test.go @@ -1,6 +1,8 @@ package registry import ( + + "github.com/stacklok/toolhive/pkg/registry/types" "encoding/json" "testing" "time" @@ -11,12 +13,12 @@ import ( func TestRegistryWithRemoteServers(t *testing.T) { t.Parallel() - registry := &Registry{ + registry := &types.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: map[string]*ImageMetadata{ + Servers: map[string]*types.ImageMetadata{ "container-server": { - BaseServerMetadata: BaseServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "container-server", Description: "A containerized MCP server", Tier: "Official", @@ -28,9 +30,9 @@ func TestRegistryWithRemoteServers(t *testing.T) { TargetPort: 8080, }, }, - RemoteServers: map[string]*RemoteServerMetadata{ + RemoteServers: map[string]*types.RemoteServerMetadata{ "remote-server": { - BaseServerMetadata: BaseServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "remote-server", Description: "A remote MCP server", Tier: "Community", @@ -48,7 +50,7 @@ func TestRegistryWithRemoteServers(t *testing.T) { require.NoError(t, err) // Test JSON unmarshaling - var decoded Registry + var decoded types.Registry err = json.Unmarshal(data, &decoded) require.NoError(t, err) @@ -61,8 +63,8 @@ func TestRegistryWithRemoteServers(t *testing.T) { func TestRemoteServerMetadataWithHeaders(t *testing.T) { t.Parallel() - remote := &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ + remote := &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "auth-server", Description: "Remote server with authentication headers", Tier: "Official", @@ -71,7 +73,7 @@ func TestRemoteServerMetadataWithHeaders(t *testing.T) { Tools: []string{"secure_tool"}, }, URL: "https://secure.example.com/mcp", - Headers: []*Header{ + Headers: []*types.Header{ { Name: "X-API-Key", Description: "API key for authentication", @@ -93,7 +95,7 @@ func TestRemoteServerMetadataWithHeaders(t *testing.T) { require.NoError(t, err) // Test JSON unmarshaling - var decoded RemoteServerMetadata + var decoded types.RemoteServerMetadata err = json.Unmarshal(data, &decoded) require.NoError(t, err) @@ -110,12 +112,12 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { t.Parallel() tests := []struct { name string - remote *RemoteServerMetadata + remote *types.RemoteServerMetadata }{ { name: "OIDC configuration", - remote: &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ + remote: &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "oidc-server", Description: "Remote server with OIDC authentication", Tier: "Official", @@ -124,7 +126,7 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { Tools: []string{"oidc_tool"}, }, URL: "https://oidc.example.com/mcp", - OAuthConfig: &OAuthConfig{ + OAuthConfig: &types.OAuthConfig{ Issuer: "https://auth.example.com", ClientID: "mcp-client-id", Scopes: []string{"openid", "profile", "email"}, @@ -134,8 +136,8 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { }, { name: "Manual OAuth configuration", - remote: &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ + remote: &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "oauth-server", Description: "Remote server with manual OAuth endpoints", Tier: "Community", @@ -144,7 +146,7 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { Tools: []string{"oauth_tool"}, }, URL: "https://oauth.example.com/mcp", - OAuthConfig: &OAuthConfig{ + OAuthConfig: &types.OAuthConfig{ AuthorizeURL: "https://custom.example.com/oauth/authorize", TokenURL: "https://custom.example.com/oauth/token", ClientID: "custom-client-id", @@ -164,7 +166,7 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { require.NoError(t, err) // Test JSON unmarshaling - var decoded RemoteServerMetadata + var decoded types.RemoteServerMetadata err = json.Unmarshal(data, &decoded) require.NoError(t, err) @@ -188,14 +190,14 @@ func TestRemoteServerMetadataWithOAuth(t *testing.T) { func TestBaseServerMetadataInheritance(t *testing.T) { t.Parallel() // Test that both ImageMetadata and RemoteServerMetadata properly inherit BaseServerMetadata - baseFields := BaseServerMetadata{ + baseFields := types.BaseServerMetadata{ Name: "test-server", Description: "Test server description", Tier: "Official", Status: "Active", Transport: "sse", Tools: []string{"tool1", "tool2"}, - Metadata: &Metadata{ + Metadata: &types.Metadata{ Stars: 100, Pulls: 5000, LastUpdated: time.Now().Format(time.RFC3339), @@ -208,7 +210,7 @@ func TestBaseServerMetadataInheritance(t *testing.T) { } // Test with ImageMetadata - image := &ImageMetadata{ + image := &types.ImageMetadata{ BaseServerMetadata: baseFields, Image: "mcp/test:latest", } @@ -216,7 +218,7 @@ func TestBaseServerMetadataInheritance(t *testing.T) { imageData, err := json.Marshal(image) require.NoError(t, err) - var decodedImage ImageMetadata + var decodedImage types.ImageMetadata err = json.Unmarshal(imageData, &decodedImage) require.NoError(t, err) @@ -229,7 +231,7 @@ func TestBaseServerMetadataInheritance(t *testing.T) { assert.Equal(t, "mcp/test:latest", decodedImage.Image) // Test with RemoteServerMetadata - remote := &RemoteServerMetadata{ + remote := &types.RemoteServerMetadata{ BaseServerMetadata: baseFields, URL: "https://api.example.com/mcp", } @@ -237,7 +239,7 @@ func TestBaseServerMetadataInheritance(t *testing.T) { remoteData, err := json.Marshal(remote) require.NoError(t, err) - var decodedRemote RemoteServerMetadata + var decodedRemote types.RemoteServerMetadata err = json.Unmarshal(remoteData, &decodedRemote) require.NoError(t, err) @@ -256,8 +258,8 @@ func TestRemoteServerTransportValidation(t *testing.T) { validTransports := []string{"sse", "streamable-http"} for _, transport := range validTransports { - remote := &RemoteServerMetadata{ - BaseServerMetadata: BaseServerMetadata{ + remote := &types.RemoteServerMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "test-server", Description: "Test server", Tier: "Official", @@ -271,7 +273,7 @@ func TestRemoteServerTransportValidation(t *testing.T) { data, err := json.Marshal(remote) require.NoError(t, err) - var decoded RemoteServerMetadata + var decoded types.RemoteServerMetadata err = json.Unmarshal(data, &decoded) require.NoError(t, err) assert.Equal(t, transport, decoded.Transport) @@ -283,7 +285,7 @@ func TestRemoteServerTransportValidation(t *testing.T) { func TestHeaderSecretField(t *testing.T) { t.Parallel() - header := &Header{ + header := &types.Header{ Name: "Authorization", Description: "Bearer token for authentication", Required: true, @@ -293,7 +295,7 @@ func TestHeaderSecretField(t *testing.T) { data, err := json.Marshal(header) require.NoError(t, err) - var decoded Header + var decoded types.Header err = json.Unmarshal(data, &decoded) require.NoError(t, err) @@ -304,7 +306,7 @@ func TestHeaderSecretField(t *testing.T) { func TestMetadataParsedTime(t *testing.T) { t.Parallel() now := time.Now().Truncate(time.Second) - metadata := &Metadata{ + metadata := &types.Metadata{ Stars: 100, Pulls: 5000, LastUpdated: now.Format(time.RFC3339), diff --git a/pkg/registry/upstream_conversion_test.go b/pkg/registry/upstream_conversion_test.go index 4332b6204..4d7035d12 100644 --- a/pkg/registry/upstream_conversion_test.go +++ b/pkg/registry/upstream_conversion_test.go @@ -1,6 +1,8 @@ package registry import ( + + "github.com/stacklok/toolhive/pkg/registry/types" "testing" "github.com/stretchr/testify/assert" @@ -60,7 +62,7 @@ func TestConvertUpstreamToToolhive_DockerPackage(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - imageMetadata, ok := result.(*ImageMetadata) + imageMetadata, ok := result.(*types.ImageMetadata) require.True(t, ok, "Expected ImageMetadata") assert.Equal(t, "io.modelcontextprotocol/filesystem", imageMetadata.GetName()) @@ -122,7 +124,7 @@ func TestConvertUpstreamToToolhive_NPMPackage(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - imageMetadata, ok := result.(*ImageMetadata) + imageMetadata, ok := result.(*types.ImageMetadata) require.True(t, ok, "Expected ImageMetadata") assert.Equal(t, "io.modelcontextprotocol/brave-search", imageMetadata.GetName()) @@ -182,7 +184,7 @@ func TestConvertUpstreamToToolhive_RemoteServer(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - remoteMetadata, ok := result.(*RemoteServerMetadata) + remoteMetadata, ok := result.(*types.RemoteServerMetadata) require.True(t, ok, "Expected RemoteServerMetadata") assert.Equal(t, "Remote Filesystem Server", remoteMetadata.GetName()) @@ -205,8 +207,8 @@ func TestConvertUpstreamToToolhive_RemoteServer(t *testing.T) { func TestConvertToolhiveToUpstream_ImageMetadata(t *testing.T) { t.Parallel() - imageMetadata := &ImageMetadata{ - BaseServerMetadata: BaseServerMetadata{ + imageMetadata := &types.ImageMetadata{ + BaseServerMetadata: types.BaseServerMetadata{ Name: "test-server", Description: "Test MCP server", Tier: "Official", @@ -228,7 +230,7 @@ func TestConvertToolhiveToUpstream_ImageMetadata(t *testing.T) { }, }, }, - EnvVars: []*EnvVar{ + EnvVars: []*types.EnvVar{ { Name: "TEST_VAR", Description: "Test environment variable", @@ -506,7 +508,7 @@ func TestConvertUpstreamToToolhive_PythonPackage(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - imageMetadata, ok := result.(*ImageMetadata) + imageMetadata, ok := result.(*types.ImageMetadata) require.True(t, ok, "Expected ImageMetadata") assert.Equal(t, "weather-mcp-server", imageMetadata.GetName()) From 996c200bf7182b0f1e97ac12742c6f0bbb221796 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 13:38:43 +0200 Subject: [PATCH 07/20] Config changes now take effect immediately without a restart Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/config.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cmd/thv/app/config.go b/cmd/thv/app/config.go index 0ff1def1d..66e58d4f8 100644 --- a/cmd/thv/app/config.go +++ b/cmd/thv/app/config.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/stacklok/toolhive/pkg/config" + "github.com/stacklok/toolhive/pkg/registry" ) var configCmd = &cobra.Command{ @@ -190,6 +191,8 @@ func setRegistryCmdFunc(_ *cobra.Command, args []string) error { if err != nil { return err } + // Reset the cached provider so it re-initializes with the new config + registry.ResetDefaultProvider() fmt.Printf("Successfully set registry URL: %s\n", cleanPath) if allowPrivateRegistryIp { fmt.Print("Successfully enabled use of private IP addresses for the remote registry\n") @@ -201,7 +204,14 @@ func setRegistryCmdFunc(_ *cobra.Command, args []string) error { } return nil case config.RegistryTypeFile: - return provider.SetRegistryFile(cleanPath) + err := provider.SetRegistryFile(cleanPath) + if err != nil { + return err + } + // Reset the cached provider so it re-initializes with the new config + registry.ResetDefaultProvider() + fmt.Printf("Successfully set local registry file: %s\n", cleanPath) + return nil default: return fmt.Errorf("unsupported registry type") } @@ -216,6 +226,9 @@ func setRegistryAPICmdFunc(_ *cobra.Command, args []string) error { return err } + // Reset the cached provider so it re-initializes with the new config + registry.ResetDefaultProvider() + fmt.Printf("Successfully set registry API endpoint: %s\n", apiURL) if allowPrivateRegistryIp { fmt.Print("Successfully enabled use of private IP addresses for the registry API\n") @@ -260,6 +273,9 @@ func unsetRegistryCmdFunc(_ *cobra.Command, _ []string) error { return fmt.Errorf("failed to update configuration: %w", err) } + // Reset the cached provider so it re-initializes with the new config + registry.ResetDefaultProvider() + if url != "" { fmt.Printf("Successfully removed registry URL: %s\n", url) } else if localPath != "" { From 902a4e0635311144faa612dedf3d323ae6c4399e Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 14:53:49 +0200 Subject: [PATCH 08/20] Fix a small issue with converting the names Signed-off-by: Radoslav Dimitrov --- pkg/registry/api/client.go | 6 +++--- .../server_to_image/expected_github.json | 1 + .../server_to_remote/expected_example.json | 1 + pkg/registry/converters/upstream_to_toolhive.go | 16 ++++++++++++++-- pkg/registry/provider_api.go | 5 ----- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/registry/api/client.go b/pkg/registry/api/client.go index 43adf347d..2f0e66e06 100644 --- a/pkg/registry/api/client.go +++ b/pkg/registry/api/client.go @@ -77,7 +77,7 @@ func NewClient(baseURL string, allowPrivateIp bool) (Client, error) { func (c *mcpRegistryClient) GetServer(ctx context.Context, name string) (*v0.ServerJSON, error) { // URL encode the server name to handle special characters encodedName := url.PathEscape(name) - endpoint := fmt.Sprintf("%s/v0/servers/%s/versions/latest", c.baseURL, encodedName) + endpoint := fmt.Sprintf("%s/v0.1/servers/%s/versions/latest", c.baseURL, encodedName) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { @@ -148,7 +148,7 @@ func (c *mcpRegistryClient) ListServers(ctx context.Context, opts *ListOptions) func (c *mcpRegistryClient) fetchServersPage( ctx context.Context, cursor string, opts *ListOptions, ) ([]*v0.ServerJSON, string, error) { - endpoint := fmt.Sprintf("%s/v0/servers", c.baseURL) + endpoint := fmt.Sprintf("%s/v0.1/servers", c.baseURL) // Build query parameters params := url.Values{} @@ -207,7 +207,7 @@ func (c *mcpRegistryClient) SearchServers(ctx context.Context, query string) ([] params.Add("search", query) params.Add("version", "latest") - endpoint := fmt.Sprintf("%s/v0/servers?%s", c.baseURL, params.Encode()) + endpoint := fmt.Sprintf("%s/v0.1/servers?%s", c.baseURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { diff --git a/pkg/registry/converters/testdata/server_to_image/expected_github.json b/pkg/registry/converters/testdata/server_to_image/expected_github.json index 875d3ecbc..7e518f7fa 100644 --- a/pkg/registry/converters/testdata/server_to_image/expected_github.json +++ b/pkg/registry/converters/testdata/server_to_image/expected_github.json @@ -1,4 +1,5 @@ { + "name": "github", "description": "Provides integration with GitHub's APIs", "tier": "Official", "status": "Active", diff --git a/pkg/registry/converters/testdata/server_to_remote/expected_example.json b/pkg/registry/converters/testdata/server_to_remote/expected_example.json index 24b06f5ff..d868edcfa 100644 --- a/pkg/registry/converters/testdata/server_to_remote/expected_example.json +++ b/pkg/registry/converters/testdata/server_to_remote/expected_example.json @@ -1,4 +1,5 @@ { + "name": "example-remote", "description": "Example remote MCP server accessed via SSE", "tier": "Community", "status": "active", diff --git a/pkg/registry/converters/upstream_to_toolhive.go b/pkg/registry/converters/upstream_to_toolhive.go index 52ebdf593..3898cfbab 100644 --- a/pkg/registry/converters/upstream_to_toolhive.go +++ b/pkg/registry/converters/upstream_to_toolhive.go @@ -26,9 +26,15 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*types.ImageMet return nil, err } + // Use Title if available, otherwise extract simple name from reverse-DNS Name + displayName := serverJSON.Title + if displayName == "" { + displayName = ExtractServerName(serverJSON.Name) + } + imageMetadata := &types.ImageMetadata{ BaseServerMetadata: types.BaseServerMetadata{ - Name: serverJSON.Title, + Name: displayName, Description: serverJSON.Description, Transport: pkg.Transport.Type, }, @@ -143,9 +149,15 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*types.R remote := serverJSON.Remotes[0] // Use first remote + // Use Title if available, otherwise extract simple name from reverse-DNS Name + displayName := serverJSON.Title + if displayName == "" { + displayName = ExtractServerName(serverJSON.Name) + } + remoteMetadata := &types.RemoteServerMetadata{ BaseServerMetadata: types.BaseServerMetadata{ - Name: serverJSON.Title, + Name: displayName, Description: serverJSON.Description, Transport: remote.Type, }, diff --git a/pkg/registry/provider_api.go b/pkg/registry/provider_api.go index 6bb85224f..ae3518e1f 100644 --- a/pkg/registry/provider_api.go +++ b/pkg/registry/provider_api.go @@ -2,7 +2,6 @@ package registry import ( "context" - "encoding/json" "fmt" "time" @@ -206,10 +205,6 @@ func ConvertServerJSON(serverJSON *v0.ServerJSON) (types.ServerMetadata, error) return nil, err } - // TEMPORARY DEBUG: Print after conversion - afterJSON, _ := json.MarshalIndent(result, "", " ") - fmt.Printf("\n=== AFTER CONVERSION ===\n%s\n", string(afterJSON)) - return result, nil } From 2c3ed70bdca0f5874f5d25ba52e212392dad76c4 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 17:08:32 +0200 Subject: [PATCH 09/20] Normalise the full name instead of simplifying it Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/run.go | 17 +++++++++++++---- .../server_to_image/expected_github.json | 2 +- .../server_to_remote/expected_example.json | 2 +- pkg/registry/converters/upstream_to_toolhive.go | 8 ++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cmd/thv/app/run.go b/cmd/thv/app/run.go index 078c533db..81502080e 100644 --- a/cmd/thv/app/run.go +++ b/cmd/thv/app/run.go @@ -21,6 +21,7 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/process" + "github.com/stacklok/toolhive/pkg/registry" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/validation" "github.com/stacklok/toolhive/pkg/workloads" @@ -280,11 +281,19 @@ func getworkloadDefaultName(ctx context.Context, serverOrImage string) string { return name } - // Check if it's a server name from registry + // Check if it's a server name from registry (including reverse-DNS names with slashes) if !strings.Contains(serverOrImage, "://") && !strings.Contains(serverOrImage, ":") { - // Simple server name (no protocol, no slashes, no colons), return as-is - if !strings.Contains(serverOrImage, "/") { - return serverOrImage + // Check if this is a registry server name by attempting to look it up + provider, err := registry.GetDefaultProvider() + if err == nil { + _, err := provider.GetServer(serverOrImage) + if err == nil { + // It's a valid registry server name - sanitize for container/filesystem use + // Replace dots and slashes with dashes to create a valid workload name + sanitized := strings.ReplaceAll(serverOrImage, ".", "-") + sanitized = strings.ReplaceAll(sanitized, "/", "-") + return sanitized + } } } diff --git a/pkg/registry/converters/testdata/server_to_image/expected_github.json b/pkg/registry/converters/testdata/server_to_image/expected_github.json index 7e518f7fa..f0a3764d0 100644 --- a/pkg/registry/converters/testdata/server_to_image/expected_github.json +++ b/pkg/registry/converters/testdata/server_to_image/expected_github.json @@ -1,5 +1,5 @@ { - "name": "github", + "name": "io.github.stacklok/github", "description": "Provides integration with GitHub's APIs", "tier": "Official", "status": "Active", diff --git a/pkg/registry/converters/testdata/server_to_remote/expected_example.json b/pkg/registry/converters/testdata/server_to_remote/expected_example.json index d868edcfa..5d5977a06 100644 --- a/pkg/registry/converters/testdata/server_to_remote/expected_example.json +++ b/pkg/registry/converters/testdata/server_to_remote/expected_example.json @@ -1,5 +1,5 @@ { - "name": "example-remote", + "name": "io.github.stacklok/example-remote", "description": "Example remote MCP server accessed via SSE", "tier": "Community", "status": "active", diff --git a/pkg/registry/converters/upstream_to_toolhive.go b/pkg/registry/converters/upstream_to_toolhive.go index 3898cfbab..ce72a4d3f 100644 --- a/pkg/registry/converters/upstream_to_toolhive.go +++ b/pkg/registry/converters/upstream_to_toolhive.go @@ -26,10 +26,10 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*types.ImageMet return nil, err } - // Use Title if available, otherwise extract simple name from reverse-DNS Name + // Use Title if available, otherwise use full reverse-DNS Name to avoid conflicts displayName := serverJSON.Title if displayName == "" { - displayName = ExtractServerName(serverJSON.Name) + displayName = serverJSON.Name } imageMetadata := &types.ImageMetadata{ @@ -149,10 +149,10 @@ func ServerJSONToRemoteServerMetadata(serverJSON *upstream.ServerJSON) (*types.R remote := serverJSON.Remotes[0] // Use first remote - // Use Title if available, otherwise extract simple name from reverse-DNS Name + // Use Title if available, otherwise use full reverse-DNS Name to avoid conflicts displayName := serverJSON.Title if displayName == "" { - displayName = ExtractServerName(serverJSON.Name) + displayName = serverJSON.Name } remoteMetadata := &types.RemoteServerMetadata{ From 53085633799b3767a05abf250157498d03a63334 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Wed, 5 Nov 2025 17:41:46 +0200 Subject: [PATCH 10/20] Fix imports after rebase - update to use pkg/registry/types --- pkg/auth/remote/config.go | 10 +++++----- pkg/runner/config.go | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/auth/remote/config.go b/pkg/auth/remote/config.go index 9f0b25edc..5b9b85fe5 100644 --- a/pkg/auth/remote/config.go +++ b/pkg/auth/remote/config.go @@ -8,7 +8,7 @@ import ( "time" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/validation" ) @@ -33,10 +33,10 @@ type Config struct { TokenURL string `json:"token_url,omitempty" yaml:"token_url,omitempty"` // Headers for HTTP requests - Headers []*registry.Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Headers []*types.Header `json:"headers,omitempty" yaml:"headers,omitempty"` // Environment variables for the client - EnvVars []*registry.EnvVar `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` + EnvVars []*types.EnvVar `json:"env_vars,omitempty" yaml:"env_vars,omitempty"` // OAuth parameters for server-specific customization OAuthParams map[string]string `json:"oauth_params,omitempty" yaml:"oauth_params,omitempty"` @@ -67,8 +67,8 @@ func (r *Config) UnmarshalJSON(data []byte) error { Issuer string `json:"Issuer,omitempty"` AuthorizeURL string `json:"AuthorizeURL,omitempty"` TokenURL string `json:"TokenURL,omitempty"` - Headers []*registry.Header `json:"Headers,omitempty"` - EnvVars []*registry.EnvVar `json:"EnvVars,omitempty"` + Headers []*types.Header `json:"Headers,omitempty"` + EnvVars []*types.EnvVar `json:"EnvVars,omitempty"` OAuthParams map[string]string `json:"OAuthParams,omitempty"` } diff --git a/pkg/runner/config.go b/pkg/runner/config.go index 3716b48a7..f4897c233 100644 --- a/pkg/runner/config.go +++ b/pkg/runner/config.go @@ -20,7 +20,6 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" "github.com/stacklok/toolhive/pkg/permissions" - regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/secrets" "github.com/stacklok/toolhive/pkg/state" "github.com/stacklok/toolhive/pkg/telemetry" From 6e123ae47e66e8af4a27fcb54971467852ed40b9 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 6 Nov 2025 09:58:35 +0200 Subject: [PATCH 11/20] Fix imports after package reorganization rebase Signed-off-by: Radoslav Dimitrov --- cmd/regup/app/update.go | 30 ++-- cmd/regup/app/update_test.go | 24 +-- .../pkg/filtering/filter_service.go | 14 +- .../pkg/filtering/filter_service_test.go | 72 ++++----- cmd/thv-operator/pkg/sources/api_toolhive.go | 28 ++-- cmd/thv-operator/pkg/sources/git_test.go | 24 +-- .../pkg/sources/mocks/mock_source_handler.go | 6 +- .../pkg/sources/mocks/mock_storage_manager.go | 8 +- .../pkg/sources/storage_manager.go | 12 +- .../pkg/sources/storage_manager_test.go | 28 ++-- cmd/thv-operator/pkg/sources/testutils.go | 27 ++-- .../pkg/sources/testutils_test.go | 7 +- cmd/thv-operator/pkg/sources/types.go | 25 ++-- cmd/thv-operator/pkg/sources/types_test.go | 40 ++--- .../pkg/validation/image_validation.go | 6 +- .../pkg/validation/image_validation_test.go | 30 ++-- cmd/thv-proxyrunner/app/run.go | 4 +- cmd/thv/app/run.go | 2 +- pkg/api/v1/registry.go | 140 ++++++++++-------- pkg/api/v1/workload_types.go | 6 +- pkg/api/v1/workloads_test.go | 10 +- pkg/auth/remote/config.go | 28 ++-- pkg/mcp/server/handler_mock_test.go | 28 ++-- pkg/mcp/server/handler_test.go | 38 ++--- .../converters/converters_fixture_test.go | 3 +- pkg/registry/converters/converters_test.go | 3 +- pkg/registry/converters/integration_test.go | 3 +- .../converters/toolhive_to_upstream.go | 1 + .../converters/upstream_to_toolhive.go | 1 + pkg/registry/provider_test.go | 3 +- pkg/registry/schema_validation_test.go | 4 +- pkg/registry/types_test.go | 4 +- pkg/registry/upstream_conversion.go | 4 +- pkg/registry/upstream_conversion_test.go | 3 +- pkg/runner/config_builder_test.go | 15 +- pkg/runner/config_test.go | 9 +- pkg/runner/retriever/retriever_test.go | 3 +- 37 files changed, 360 insertions(+), 333 deletions(-) diff --git a/cmd/regup/app/update.go b/cmd/regup/app/update.go index 394ef7bce..0768db43b 100644 --- a/cmd/regup/app/update.go +++ b/cmd/regup/app/update.go @@ -16,7 +16,7 @@ import ( "github.com/stacklok/toolhive/pkg/container/verifier" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) var ( @@ -29,7 +29,7 @@ var ( type serverWithName struct { name string - server *registry.ImageMetadata + server *regtypes.ImageMetadata } // ProvenanceVerificationError represents an error during provenance verification @@ -96,7 +96,7 @@ func updateCmdFunc(_ *cobra.Command, _ []string) error { return saveResults(reg, updatedServers, failedServers) } -func loadRegistry() (*registry.Registry, error) { +func loadRegistry() (*regtypes.Registry, error) { registryPath := filepath.Join("pkg", "registry", "data", "registry.json") // #nosec G304 -- This is a known file path data, err := os.ReadFile(registryPath) @@ -104,7 +104,7 @@ func loadRegistry() (*registry.Registry, error) { return nil, fmt.Errorf("failed to read registry file: %w", err) } - var reg registry.Registry + var reg regtypes.Registry if err := json.Unmarshal(data, ®); err != nil { return nil, fmt.Errorf("failed to parse registry: %w", err) } @@ -112,7 +112,7 @@ func loadRegistry() (*registry.Registry, error) { return ®, nil } -func selectServersToUpdate(reg *registry.Registry) ([]serverWithName, error) { +func selectServersToUpdate(reg *regtypes.Registry) ([]serverWithName, error) { if serverName != "" { return selectSpecificServer(reg, serverName) } @@ -120,7 +120,7 @@ func selectServersToUpdate(reg *registry.Registry) ([]serverWithName, error) { return selectOldestServers(reg) } -func selectSpecificServer(reg *registry.Registry, name string) ([]serverWithName, error) { +func selectSpecificServer(reg *regtypes.Registry, name string) ([]serverWithName, error) { server, exists := reg.Servers[name] if !exists { return nil, fmt.Errorf("server '%s' not found in registry", name) @@ -129,7 +129,7 @@ func selectSpecificServer(reg *registry.Registry, name string) ([]serverWithName return []serverWithName{{name: name, server: server}}, nil } -func selectOldestServers(reg *registry.Registry) ([]serverWithName, error) { +func selectOldestServers(reg *regtypes.Registry) ([]serverWithName, error) { servers := make([]serverWithName, 0, len(reg.Servers)) for name, server := range reg.Servers { server.Name = name @@ -151,7 +151,7 @@ func selectOldestServers(reg *registry.Registry) ([]serverWithName, error) { return servers[:limit], nil } -func isOlder(serverI, serverJ *registry.ImageMetadata) bool { +func isOlder(serverI, serverJ *regtypes.ImageMetadata) bool { var lastUpdatedI, lastUpdatedJ string if serverI.Metadata != nil { @@ -180,7 +180,7 @@ func isOlder(serverI, serverJ *registry.ImageMetadata) bool { return timeI.Before(timeJ) } -func updateServers(servers []serverWithName, reg *registry.Registry) ([]string, []string) { +func updateServers(servers []serverWithName, reg *regtypes.Registry) ([]string, []string) { updatedServers := make([]string, 0, len(servers)) failedServers := make([]string, 0) @@ -209,7 +209,7 @@ func updateServers(servers []serverWithName, reg *registry.Registry) ([]string, return updatedServers, failedServers } -func saveResults(reg *registry.Registry, updatedServers []string, failedServers []string) error { +func saveResults(reg *regtypes.Registry, updatedServers []string, failedServers []string) error { // If we're in dry run mode, don't save changes if dryRun { logger.Info("Dry run completed, no changes made") @@ -235,7 +235,7 @@ func saveResults(reg *registry.Registry, updatedServers []string, failedServers } // updateServerInfo updates the GitHub stars and pulls for a server -func updateServerInfo(name string, server *registry.ImageMetadata) error { +func updateServerInfo(name string, server *regtypes.ImageMetadata) error { // Verify provenance if requested if verifyProvenance { if err := verifyServerProvenance(name, server); err != nil { @@ -254,7 +254,7 @@ func updateServerInfo(name string, server *registry.ImageMetadata) error { // Initialize metadata if it's nil if server.Metadata == nil { - server.Metadata = ®istry.Metadata{} + server.Metadata = ®types.Metadata{} } // Extract owner and repo from repository URL @@ -289,7 +289,7 @@ func updateServerInfo(name string, server *registry.ImageMetadata) error { } // verifyServerProvenance verifies the provenance information for a server -func verifyServerProvenance(name string, server *registry.ImageMetadata) error { +func verifyServerProvenance(name string, server *regtypes.ImageMetadata) error { // Skip if no provenance information if server.Provenance == nil { logger.Warnf("Server %s has no provenance information, skipping verification", name) @@ -325,7 +325,7 @@ func verifyServerProvenance(name string, server *registry.ImageMetadata) error { } // removeFailedServers removes servers that failed provenance verification from the registry -func removeFailedServers(reg *registry.Registry, failedServers []string) { +func removeFailedServers(reg *regtypes.Registry, failedServers []string) { for _, serverName := range failedServers { logger.Warnf("Removing server %s from registry due to provenance verification failure", serverName) delete(reg.Servers, serverName) @@ -420,7 +420,7 @@ func getGitHubRepoInfo(owner, repo, serverName string, currentPulls int) (stars } // saveRegistry saves the registry to the filesystem while preserving the order of entries -func saveRegistry(reg *registry.Registry, updatedServers []string, failedServers []string) error { +func saveRegistry(reg *regtypes.Registry, updatedServers []string, failedServers []string) error { // Find the registry file path registryPath := filepath.Join("pkg", "registry", "data", "registry.json") diff --git a/cmd/regup/app/update_test.go b/cmd/regup/app/update_test.go index 606be5550..f350ca5c0 100644 --- a/cmd/regup/app/update_test.go +++ b/cmd/regup/app/update_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) //nolint:paralleltest // This test manages temporary directories and cannot run in parallel @@ -141,7 +141,7 @@ func TestServerSelection(t *testing.T) { data, err := os.ReadFile(registryPath) require.NoError(t, err) - var reg registry.Registry + var reg regtypes.Registry err = json.Unmarshal(data, ®) require.NoError(t, err) @@ -170,15 +170,15 @@ func setupTestRegistryWithMultipleServers(t *testing.T) (string, func()) { require.NoError(t, err) // Create test registry with multiple servers - testRegistry := ®istry.Registry{ + testRegistry := ®types.Registry{ LastUpdated: "2025-06-16T12:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "github": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "github", Description: "GitHub MCP server", RepositoryURL: "https://github.com/github/github-mcp-server", - Metadata: ®istry.Metadata{ + Metadata: ®types.Metadata{ Stars: 100, Pulls: 5000, LastUpdated: "2025-06-16T12:00:00Z", // Older @@ -187,11 +187,11 @@ func setupTestRegistryWithMultipleServers(t *testing.T) (string, func()) { Image: "ghcr.io/github/github-mcp-server:latest", }, "gitlab": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "gitlab", Description: "GitLab MCP server", RepositoryURL: "https://github.com/example/gitlab-mcp-server", - Metadata: ®istry.Metadata{ + Metadata: ®types.Metadata{ Stars: 50, Pulls: 2000, LastUpdated: "2025-06-17T12:00:00Z", // Newer @@ -200,11 +200,11 @@ func setupTestRegistryWithMultipleServers(t *testing.T) (string, func()) { Image: "mcp/gitlab:latest", }, "fetch": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "fetch", Description: "Fetch MCP server", RepositoryURL: "https://github.com/example/fetch-mcp-server", - Metadata: ®istry.Metadata{ + Metadata: ®types.Metadata{ Stars: 25, Pulls: 1000, LastUpdated: "2025-06-15T12:00:00Z", // Oldest @@ -241,9 +241,9 @@ func setupEmptyTestRegistry(t *testing.T) (string, func()) { require.NoError(t, err) // Create empty test registry - testRegistry := ®istry.Registry{ + testRegistry := ®types.Registry{ LastUpdated: "2025-06-16T12:00:00Z", - Servers: map[string]*registry.ImageMetadata{}, + Servers: map[string]*regtypes.ImageMetadata{}, } // Write registry file diff --git a/cmd/thv-operator/pkg/filtering/filter_service.go b/cmd/thv-operator/pkg/filtering/filter_service.go index ba7345501..114c3e953 100644 --- a/cmd/thv-operator/pkg/filtering/filter_service.go +++ b/cmd/thv-operator/pkg/filtering/filter_service.go @@ -8,13 +8,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) // FilterService coordinates name and tag filtering to apply registry filters type FilterService interface { // ApplyFilters filters the registry based on MCPRegistry filter configuration - ApplyFilters(ctx context.Context, reg *registry.Registry, filter *mcpv1alpha1.RegistryFilter) (*registry.Registry, error) + ApplyFilters(ctx context.Context, reg *regtypes.Registry, filter *mcpv1alpha1.RegistryFilter) (*regtypes.Registry, error) } // DefaultFilterService implements filtering coordination using name and tag filters @@ -49,8 +49,8 @@ func NewFilterService(nameFilter NameFilter, tagFilter TagFilter) *DefaultFilter // 5. Return the filtered registry func (s *DefaultFilterService) ApplyFilters( ctx context.Context, - reg *registry.Registry, - filter *mcpv1alpha1.RegistryFilter) (*registry.Registry, error) { + reg *regtypes.Registry, + filter *mcpv1alpha1.RegistryFilter) (*regtypes.Registry, error) { ctxLogger := log.FromContext(ctx) // If no filter is specified, return original registry @@ -64,11 +64,11 @@ func (s *DefaultFilterService) ApplyFilters( "originalRemoteServerCount", len(reg.RemoteServers)) // Create a new filtered registry with same metadata - filteredRegistry := ®istry.Registry{ + filteredRegistry := ®types.Registry{ Version: reg.Version, LastUpdated: reg.LastUpdated, - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), Groups: reg.Groups, // Groups are not filtered for now } diff --git a/cmd/thv-operator/pkg/filtering/filter_service_test.go b/cmd/thv-operator/pkg/filtering/filter_service_test.go index ad5e29291..146a647c9 100644 --- a/cmd/thv-operator/pkg/filtering/filter_service_test.go +++ b/cmd/thv-operator/pkg/filtering/filter_service_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestNewDefaultFilterService(t *testing.T) { @@ -39,21 +39,21 @@ func TestDefaultFilterService_ApplyFilters_NoFilter(t *testing.T) { ctx := context.Background() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "postgres": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "postgres", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*regtypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web", "api"}, }, @@ -110,42 +110,42 @@ func TestDefaultFilterService_ApplyFilters_NameFiltering(t *testing.T) { t.Parallel() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database"}, }, Image: "postgres:latest", }, "mysql-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "mysql-server", Tags: []string{"database"}, }, Image: "mysql:latest", }, "redis-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "redis-experimental", Tags: []string{"cache"}, }, Image: "redis:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*regtypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web"}, }, URL: "https://example.com", }, "admin-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "admin-experimental", Tags: []string{"admin"}, }, @@ -223,42 +223,42 @@ func TestDefaultFilterService_ApplyFilters_TagFiltering(t *testing.T) { t.Parallel() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, "mysql-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "mysql-server", Tags: []string{"database", "deprecated"}, }, Image: "mysql:latest", }, "redis-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "redis-server", Tags: []string{"cache"}, }, Image: "redis:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*regtypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web", "api"}, }, URL: "https://example.com", }, "legacy-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "legacy-api", Tags: []string{"web", "deprecated"}, }, @@ -300,35 +300,35 @@ func TestDefaultFilterService_ApplyFilters_CombinedFiltering(t *testing.T) { ctx := context.Background() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, "postgres-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "postgres-experimental", Tags: []string{"database", "experimental"}, }, Image: "postgres:experimental", }, "web-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "web-server", Tags: []string{"web", "api"}, }, Image: "nginx:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*regtypes.RemoteServerMetadata{ "database-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "database-api", Tags: []string{"database", "api"}, }, @@ -372,11 +372,11 @@ func TestDefaultFilterService_ApplyFilters_EmptyRegistry(t *testing.T) { ctx := context.Background() // Create empty registry - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } filter := &mcpv1alpha1.RegistryFilter{ @@ -401,18 +401,18 @@ func TestDefaultFilterService_ApplyFilters_PreservesMetadata(t *testing.T) { ctx := context.Background() // Create registry with groups - groups := []*registry.Group{ + groups := []*regtypes.Group{ { Name: "test-group", Description: "Test group", }, } - originalRegistry := ®istry.Registry{ + originalRegistry := ®types.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), Groups: groups, } diff --git a/cmd/thv-operator/pkg/sources/api_toolhive.go b/cmd/thv-operator/pkg/sources/api_toolhive.go index dcec15627..ac5f3a2fb 100644 --- a/cmd/thv-operator/pkg/sources/api_toolhive.go +++ b/cmd/thv-operator/pkg/sources/api_toolhive.go @@ -14,7 +14,7 @@ import ( mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" "github.com/stacklok/toolhive/cmd/thv-operator/pkg/httpclient" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) // ToolHiveAPIHandler handles registry data from ToolHive Registry API endpoints @@ -178,14 +178,14 @@ func (h *ToolHiveAPIHandler) convertToToolhiveRegistry( ctx context.Context, baseURL string, response *ListServersResponse, -) (*registry.Registry, error) { +) (*regtypes.Registry, error) { logger := log.FromContext(ctx) - toolhiveRegistry := ®istry.Registry{ + toolhiveRegistry := ®types.Registry{ Version: "1.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } // Fetch detailed information for each server in parallel @@ -199,7 +199,7 @@ func (h *ToolHiveAPIHandler) fetchServerDetailsParallel( ctx context.Context, baseURL string, servers []ServerSummaryResponse, - toolhiveRegistry *registry.Registry, + toolhiveRegistry *regtypes.Registry, logger logr.Logger, ) { // Limit concurrent requests to avoid overwhelming the API @@ -268,9 +268,9 @@ func (h *ToolHiveAPIHandler) fetchServerDetailsParallel( } // addServerFromSummary adds a server using only summary data (fallback) -func (*ToolHiveAPIHandler) addServerFromSummary(reg *registry.Registry, summary *ServerSummaryResponse) { - imageMetadata := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func (*ToolHiveAPIHandler) addServerFromSummary(reg *regtypes.Registry, summary *ServerSummaryResponse) { + imageMetadata := ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: summary.Name, Description: summary.Description, Tier: summary.Tier, @@ -284,9 +284,9 @@ func (*ToolHiveAPIHandler) addServerFromSummary(reg *registry.Registry, summary } // addServerFromDetail adds a server using full detail data -func (*ToolHiveAPIHandler) addServerFromDetail(reg *registry.Registry, detail *ServerDetailResponse) { - imageMetadata := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func (*ToolHiveAPIHandler) addServerFromDetail(reg *regtypes.Registry, detail *ServerDetailResponse) { + imageMetadata := ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: detail.Name, Description: detail.Description, Tier: detail.Tier, @@ -304,9 +304,9 @@ func (*ToolHiveAPIHandler) addServerFromDetail(reg *registry.Registry, detail *S // Add environment variables if present if len(detail.EnvVars) > 0 { - imageMetadata.EnvVars = make([]*registry.EnvVar, len(detail.EnvVars)) + imageMetadata.EnvVars = make([]*regtypes.EnvVar, len(detail.EnvVars)) for i, envVar := range detail.EnvVars { - imageMetadata.EnvVars[i] = ®istry.EnvVar{ + imageMetadata.EnvVars[i] = ®types.EnvVar{ Name: envVar.Name, Description: envVar.Description, Required: envVar.Required, diff --git a/cmd/thv-operator/pkg/sources/git_test.go b/cmd/thv-operator/pkg/sources/git_test.go index b80274d86..d14b45b4c 100644 --- a/cmd/thv-operator/pkg/sources/git_test.go +++ b/cmd/thv-operator/pkg/sources/git_test.go @@ -14,7 +14,7 @@ import ( mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" "github.com/stacklok/toolhive/cmd/thv-operator/pkg/git" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) const ( @@ -66,12 +66,12 @@ type MockSourceDataValidator struct { mock.Mock } -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*regtypes.Registry, error) { args := m.Called(data, format) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*registry.Registry), args.Error(1) + return args.Get(0).(*regtypes.Registry), args.Error(1) } func TestNewGitSourceHandler(t *testing.T) { @@ -288,10 +288,10 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { @@ -329,10 +329,10 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { @@ -699,10 +699,10 @@ func TestGitSourceHandler_CleanupFailure(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } mockGitClient.On("Clone", mock.Anything, mock.Anything).Return(repoInfo, nil) diff --git a/cmd/thv-operator/pkg/sources/mocks/mock_source_handler.go b/cmd/thv-operator/pkg/sources/mocks/mock_source_handler.go index 430db1ee4..b9ff6b5cc 100644 --- a/cmd/thv-operator/pkg/sources/mocks/mock_source_handler.go +++ b/cmd/thv-operator/pkg/sources/mocks/mock_source_handler.go @@ -15,7 +15,7 @@ import ( v1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" sources "github.com/stacklok/toolhive/cmd/thv-operator/pkg/sources" - registry "github.com/stacklok/toolhive/pkg/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -44,10 +44,10 @@ func (m *MockSourceDataValidator) EXPECT() *MockSourceDataValidatorMockRecorder } // ValidateData mocks base method. -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*types.Registry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateData", data, format) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*types.Registry) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/cmd/thv-operator/pkg/sources/mocks/mock_storage_manager.go b/cmd/thv-operator/pkg/sources/mocks/mock_storage_manager.go index c890c228e..effa10fdf 100644 --- a/cmd/thv-operator/pkg/sources/mocks/mock_storage_manager.go +++ b/cmd/thv-operator/pkg/sources/mocks/mock_storage_manager.go @@ -14,7 +14,7 @@ import ( reflect "reflect" v1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - registry "github.com/stacklok/toolhive/pkg/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -57,10 +57,10 @@ func (mr *MockStorageManagerMockRecorder) Delete(ctx, mcpRegistry any) *gomock.C } // Get mocks base method. -func (m *MockStorageManager) Get(ctx context.Context, mcpRegistry *v1alpha1.MCPRegistry) (*registry.Registry, error) { +func (m *MockStorageManager) Get(ctx context.Context, mcpRegistry *v1alpha1.MCPRegistry) (*types.Registry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, mcpRegistry) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*types.Registry) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -100,7 +100,7 @@ func (mr *MockStorageManagerMockRecorder) GetType() *gomock.Call { } // Store mocks base method. -func (m *MockStorageManager) Store(ctx context.Context, mcpRegistry *v1alpha1.MCPRegistry, reg *registry.Registry) error { +func (m *MockStorageManager) Store(ctx context.Context, mcpRegistry *v1alpha1.MCPRegistry, reg *types.Registry) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Store", ctx, mcpRegistry, reg) ret0, _ := ret[0].(error) diff --git a/cmd/thv-operator/pkg/sources/storage_manager.go b/cmd/thv-operator/pkg/sources/storage_manager.go index 614ae18ec..030f4ccc6 100644 --- a/cmd/thv-operator/pkg/sources/storage_manager.go +++ b/cmd/thv-operator/pkg/sources/storage_manager.go @@ -14,7 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) const ( @@ -32,10 +32,10 @@ const ( // StorageManager defines the interface for registry data persistence type StorageManager interface { // Store saves a Registry instance to persistent storage - Store(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry, reg *registry.Registry) error + Store(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry, reg *regtypes.Registry) error // Get retrieves and parses registry data from persistent storage - Get(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry) (*registry.Registry, error) + Get(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry) (*regtypes.Registry, error) // Delete removes registry data from persistent storage Delete(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry) error @@ -62,7 +62,7 @@ func NewConfigMapStorageManager(k8sClient client.Client, scheme *runtime.Scheme) } // Store saves a Registry instance to a ConfigMap -func (s *ConfigMapStorageManager) Store(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry, reg *registry.Registry) error { +func (s *ConfigMapStorageManager) Store(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry, reg *regtypes.Registry) error { // Serialize the registry to JSON data, err := json.Marshal(reg) if err != nil { @@ -129,7 +129,7 @@ func (s *ConfigMapStorageManager) Store(ctx context.Context, mcpRegistry *mcpv1a } // Get retrieves and parses registry data from a ConfigMap -func (s *ConfigMapStorageManager) Get(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry) (*registry.Registry, error) { +func (s *ConfigMapStorageManager) Get(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRegistry) (*regtypes.Registry, error) { configMapName := s.getConfigMapName(mcpRegistry) configMap := &corev1.ConfigMap{} @@ -148,7 +148,7 @@ func (s *ConfigMapStorageManager) Get(ctx context.Context, mcpRegistry *mcpv1alp } // Parse the JSON data into a Registry - var reg registry.Registry + var reg regtypes.Registry if err := json.Unmarshal([]byte(data), ®); err != nil { return nil, NewStorageError("parse", mcpRegistry.Name, "failed to parse registry data", err) } diff --git a/cmd/thv-operator/pkg/sources/storage_manager_test.go b/cmd/thv-operator/pkg/sources/storage_manager_test.go index 37029c87c..15191e8eb 100644 --- a/cmd/thv-operator/pkg/sources/storage_manager_test.go +++ b/cmd/thv-operator/pkg/sources/storage_manager_test.go @@ -14,7 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestNewConfigMapStorageManager(t *testing.T) { @@ -43,7 +43,7 @@ func TestConfigMapStorageManager_Store(t *testing.T) { tests := []struct { name string registry *mcpv1alpha1.MCPRegistry - registryData *registry.Registry + registryData *regtypes.Registry existingConfigMap *corev1.ConfigMap expectError bool errorContains string @@ -61,10 +61,10 @@ func TestConfigMapStorageManager_Store(t *testing.T) { }, }, }, - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), }, expectError: false, }, @@ -81,12 +81,12 @@ func TestConfigMapStorageManager_Store(t *testing.T) { }, }, }, - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), }, existingConfigMap: &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -112,12 +112,12 @@ func TestConfigMapStorageManager_Store(t *testing.T) { }, }, }, - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "test-server": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), }, expectError: false, }, @@ -192,7 +192,7 @@ func TestConfigMapStorageManager_Get(t *testing.T) { name string registry *mcpv1alpha1.MCPRegistry configMap *corev1.ConfigMap - expectedRegistry *registry.Registry + expectedRegistry *regtypes.Registry expectError bool errorContains string }{ @@ -213,9 +213,9 @@ func TestConfigMapStorageManager_Get(t *testing.T) { ConfigMapStorageDataKey: `{"version": "1.0.0", "servers": {"server1": {}}, "remoteServers": {}}`, }, }, - expectedRegistry: ®istry.Registry{ + expectedRegistry: ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": {}, }, RemoteServers: nil, // JSON unmarshaling creates nil for empty objects diff --git a/cmd/thv-operator/pkg/sources/testutils.go b/cmd/thv-operator/pkg/sources/testutils.go index 9e025fce7..b52207dee 100644 --- a/cmd/thv-operator/pkg/sources/testutils.go +++ b/cmd/thv-operator/pkg/sources/testutils.go @@ -7,12 +7,13 @@ import ( mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) // TestRegistryBuilder provides a fluent interface for building test registry data type TestRegistryBuilder struct { format string - registry *registry.Registry + registry *regtypes.Registry upstreamData []registry.UpstreamServerDetail serverCounter int } @@ -26,11 +27,11 @@ func NewTestRegistryBuilder(format string) *TestRegistryBuilder { switch format { case mcpv1alpha1.RegistryFormatToolHive, "": - builder.registry = ®istry.Registry{ + builder.registry = ®types.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } case mcpv1alpha1.RegistryFormatUpstream: builder.upstreamData = []registry.UpstreamServerDetail{} @@ -48,8 +49,8 @@ func (b *TestRegistryBuilder) WithServer(name string) *TestRegistryBuilder { switch b.format { case mcpv1alpha1.RegistryFormatToolHive, "": - b.registry.Servers[name] = ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.Servers[name] = ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test server description for %s", name), Tier: "Community", @@ -91,8 +92,8 @@ func (b *TestRegistryBuilder) WithRemoteServer(url string) *TestRegistryBuilder url = fmt.Sprintf("https://remote-server-%d.example.com", b.serverCounter-1) } - b.registry.RemoteServers[name] = ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.RemoteServers[name] = ®types.RemoteServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test remote server description for %s", name), Tier: "Community", @@ -121,8 +122,8 @@ func (b *TestRegistryBuilder) WithRemoteServerName(name, url string) *TestRegist url = fmt.Sprintf("https://%s.example.com", name) } - b.registry.RemoteServers[name] = ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.RemoteServers[name] = ®types.RemoteServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test remote server description for %s", name), Tier: "Community", @@ -157,8 +158,8 @@ func (b *TestRegistryBuilder) Empty() *TestRegistryBuilder { switch b.format { case mcpv1alpha1.RegistryFormatToolHive, "": // Keep the registry structure but clear servers - b.registry.Servers = make(map[string]*registry.ImageMetadata) - b.registry.RemoteServers = make(map[string]*registry.RemoteServerMetadata) + b.registry.Servers = make(map[string]*regtypes.ImageMetadata) + b.registry.RemoteServers = make(map[string]*regtypes.RemoteServerMetadata) case mcpv1alpha1.RegistryFormatUpstream: b.upstreamData = []registry.UpstreamServerDetail{} } @@ -204,7 +205,7 @@ func (b *TestRegistryBuilder) BuildPrettyJSON() []byte { } // GetRegistry returns the built registry (for ToolHive format only) -func (b *TestRegistryBuilder) GetRegistry() *registry.Registry { +func (b *TestRegistryBuilder) GetRegistry() *regtypes.Registry { if b.format == mcpv1alpha1.RegistryFormatToolHive || b.format == "" { return b.registry } diff --git a/cmd/thv-operator/pkg/sources/testutils_test.go b/cmd/thv-operator/pkg/sources/testutils_test.go index 22b4b88a9..68ff9553f 100644 --- a/cmd/thv-operator/pkg/sources/testutils_test.go +++ b/cmd/thv-operator/pkg/sources/testutils_test.go @@ -8,6 +8,7 @@ import ( mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestNewTestRegistryBuilder(t *testing.T) { @@ -191,7 +192,7 @@ func TestTestRegistryBuilder_WithRemoteServer(t *testing.T) { if tt.shouldAdd { assert.Len(t, builder.registry.RemoteServers, 1) - var remoteServer *registry.RemoteServerMetadata + var remoteServer *regtypes.RemoteServerMetadata for _, server := range builder.registry.RemoteServers { remoteServer = server break @@ -474,7 +475,7 @@ func TestTestRegistryBuilder_BuildJSON(t *testing.T) { switch tt.format { case mcpv1alpha1.RegistryFormatToolHive, "": // Should be a registry object - var registry registry.Registry + var registry regtypes.Registry err = json.Unmarshal(jsonData, ®istry) assert.NoError(t, err) assert.Equal(t, "1.0.0", registry.Version) @@ -506,7 +507,7 @@ func TestTestRegistryBuilder_BuildPrettyJSON(t *testing.T) { assert.Greater(t, len(prettyJSON), len(regularJSON)) // Both should unmarshal to the same data - var prettyData, regularData registry.Registry + var prettyData, regularData regtypes.Registry err1 := json.Unmarshal(prettyJSON, &prettyData) err2 := json.Unmarshal(regularJSON, ®ularData) assert.NoError(t, err1) diff --git a/cmd/thv-operator/pkg/sources/types.go b/cmd/thv-operator/pkg/sources/types.go index b55ab112e..26c94db94 100644 --- a/cmd/thv-operator/pkg/sources/types.go +++ b/cmd/thv-operator/pkg/sources/types.go @@ -7,12 +7,13 @@ import ( mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) // SourceDataValidator is an interface for validating registry source configurations type SourceDataValidator interface { // ValidateData validates raw data and returns a parsed Registry - ValidateData(data []byte, format string) (*registry.Registry, error) + ValidateData(data []byte, format string) (*regtypes.Registry, error) } //go:generate mockgen -destination=mocks/mock_source_handler.go -package=mocks -source=types.go SourceHandler,SourceHandlerFactory @@ -32,7 +33,7 @@ type SourceHandler interface { // FetchResult contains the result of a fetch operation type FetchResult struct { // Registry is the parsed registry data (replaces raw Data field) - Registry *registry.Registry + Registry *regtypes.Registry // Hash is the SHA256 hash of the serialized data for change detection Hash string @@ -46,7 +47,7 @@ type FetchResult struct { // NewFetchResult creates a new FetchResult from a Registry instance and pre-calculated hash // The hash should be calculated by the source handler to ensure consistency with CurrentHash -func NewFetchResult(reg *registry.Registry, hash string, format string) *FetchResult { +func NewFetchResult(reg *regtypes.Registry, hash string, format string) *FetchResult { serverCount := len(reg.Servers) + len(reg.RemoteServers) return &FetchResult{ @@ -72,7 +73,7 @@ func NewSourceDataValidator() SourceDataValidator { } // ValidateData validates raw data and returns a parsed Registry -func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*regtypes.Registry, error) { if len(data) == 0 { return nil, fmt.Errorf("data cannot be empty") } @@ -88,14 +89,14 @@ func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*re } // validateToolhiveFormatAndParse validates data against ToolHive registry format and returns parsed Registry -func validateToolhiveFormatAndParse(data []byte) (*registry.Registry, error) { +func validateToolhiveFormatAndParse(data []byte) (*regtypes.Registry, error) { // Use the existing schema validation from pkg/registry if err := registry.ValidateRegistrySchema(data); err != nil { return nil, err } // Parse the validated data - var reg registry.Registry + var reg regtypes.Registry if err := json.Unmarshal(data, ®); err != nil { return nil, fmt.Errorf("failed to parse ToolHive registry format: %w", err) } @@ -104,7 +105,7 @@ func validateToolhiveFormatAndParse(data []byte) (*registry.Registry, error) { } // validateUpstreamFormatAndParse validates data against upstream registry format and returns converted Registry -func validateUpstreamFormatAndParse(data []byte) (*registry.Registry, error) { +func validateUpstreamFormatAndParse(data []byte) (*regtypes.Registry, error) { // Parse as upstream format to validate structure var upstreamServers []registry.UpstreamServerDetail if err := json.Unmarshal(data, &upstreamServers); err != nil { @@ -126,11 +127,11 @@ func validateUpstreamFormatAndParse(data []byte) (*registry.Registry, error) { } // Convert upstream format to ToolHive Registry format - toolhiveRegistry := ®istry.Registry{ + toolhiveRegistry := ®types.Registry{ Version: "1.0", LastUpdated: "", // Will be set during sync - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } for _, upstreamServer := range upstreamServers { @@ -141,9 +142,9 @@ func validateUpstreamFormatAndParse(data []byte) (*registry.Registry, error) { // Add to appropriate map based on server type switch server := serverMetadata.(type) { - case *registry.ImageMetadata: + case *regtypes.ImageMetadata: toolhiveRegistry.Servers[upstreamServer.Server.Name] = server - case *registry.RemoteServerMetadata: + case *regtypes.RemoteServerMetadata: toolhiveRegistry.RemoteServers[upstreamServer.Server.Name] = server default: return nil, fmt.Errorf("unknown server type for %s", upstreamServer.Server.Name) diff --git a/cmd/thv-operator/pkg/sources/types_test.go b/cmd/thv-operator/pkg/sources/types_test.go index 9bd136755..6b589412b 100644 --- a/cmd/thv-operator/pkg/sources/types_test.go +++ b/cmd/thv-operator/pkg/sources/types_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestNewFetchResult(t *testing.T) { @@ -14,39 +14,39 @@ func TestNewFetchResult(t *testing.T) { tests := []struct { name string - registryData *registry.Registry + registryData *regtypes.Registry hash string format string }{ { name: "empty registry", - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), }, hash: "abcd1234", format: mcpv1alpha1.RegistryFormatToolHive, }, { name: "registry with servers", - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": {}, "server2": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), }, hash: "efgh5678", format: mcpv1alpha1.RegistryFormatToolHive, }, { name: "registry with remote servers", - registryData: ®istry.Registry{ + registryData: ®types.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: map[string]*registry.RemoteServerMetadata{ + Servers: make(map[string]*regtypes.ImageMetadata), + RemoteServers: map[string]*regtypes.RemoteServerMetadata{ "remote1": {}, }, }, @@ -73,16 +73,16 @@ func TestNewFetchResult(t *testing.T) { func TestFetchResultHashConsistency(t *testing.T) { t.Parallel() - registryData := ®istry.Registry{ + registryData := ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": {}, "server2": {}, "server3": {}, "server4": {}, "server5": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } hash := "consistent-hash-value" format := mcpv1alpha1.RegistryFormatToolHive @@ -100,20 +100,20 @@ func TestFetchResultHashConsistency(t *testing.T) { func TestFetchResultHashDifference(t *testing.T) { t.Parallel() - registryData1 := ®istry.Registry{ + registryData1 := ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } - registryData2 := ®istry.Registry{ + registryData2 := ®types.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "server2": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*regtypes.RemoteServerMetadata), } hash1 := "hash-for-data1" diff --git a/cmd/thv-operator/pkg/validation/image_validation.go b/cmd/thv-operator/pkg/validation/image_validation.go index e11c4521c..5f8384a77 100644 --- a/cmd/thv-operator/pkg/validation/image_validation.go +++ b/cmd/thv-operator/pkg/validation/image_validation.go @@ -13,7 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) // Sentinel errors for image validation. @@ -221,7 +221,7 @@ func (v *RegistryEnforcingValidator) checkImageInRegistry( } // Parse the registry data - var reg registry.Registry + var reg regtypes.Registry if err := json.Unmarshal([]byte(registryData), ®); err != nil { // Invalid registry data return false, fmt.Errorf("failed to parse registry data: %w", err) @@ -232,7 +232,7 @@ func (v *RegistryEnforcingValidator) checkImageInRegistry( } // findImageInRegistry searches for an image in a registry -func findImageInRegistry(reg *registry.Registry, image string) bool { +func findImageInRegistry(reg *regtypes.Registry, image string) bool { // Check top-level servers for _, server := range reg.Servers { if server.Image == image { diff --git a/cmd/thv-operator/pkg/validation/image_validation_test.go b/cmd/thv-operator/pkg/validation/image_validation_test.go index ac0266cef..8e96544d1 100644 --- a/cmd/thv-operator/pkg/validation/image_validation_test.go +++ b/cmd/thv-operator/pkg/validation/image_validation_test.go @@ -13,7 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestAlwaysAllowValidator(t *testing.T) { @@ -692,14 +692,14 @@ func TestFindImageInRegistry(t *testing.T) { tests := []struct { name string - registry *registry.Registry + registry *regtypes.Registry image string expected bool }{ { name: "finds image in top-level servers", - registry: ®istry.Registry{ - Servers: map[string]*registry.ImageMetadata{ + registry: ®types.Registry{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": { Image: "docker.io/toolhive/test:v1.0.0", }, @@ -713,12 +713,12 @@ func TestFindImageInRegistry(t *testing.T) { }, { name: "finds image in group servers", - registry: ®istry.Registry{ - Servers: map[string]*registry.ImageMetadata{}, - Groups: []*registry.Group{ + registry: ®types.Registry{ + Servers: map[string]*regtypes.ImageMetadata{}, + Groups: []*regtypes.Group{ { Name: "group1", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "group-server": { Image: "docker.io/toolhive/group:v1.0.0", }, @@ -731,16 +731,16 @@ func TestFindImageInRegistry(t *testing.T) { }, { name: "does not find missing image", - registry: ®istry.Registry{ - Servers: map[string]*registry.ImageMetadata{ + registry: ®types.Registry{ + Servers: map[string]*regtypes.ImageMetadata{ "server1": { Image: "docker.io/toolhive/test:v1.0.0", }, }, - Groups: []*registry.Group{ + Groups: []*regtypes.Group{ { Name: "group1", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*regtypes.ImageMetadata{ "group-server": { Image: "docker.io/toolhive/group:v1.0.0", }, @@ -753,15 +753,15 @@ func TestFindImageInRegistry(t *testing.T) { }, { name: "handles empty registry", - registry: ®istry.Registry{ - Servers: map[string]*registry.ImageMetadata{}, + registry: ®types.Registry{ + Servers: map[string]*regtypes.ImageMetadata{}, }, image: "docker.io/toolhive/test:v1.0.0", expected: false, }, { name: "handles nil maps", - registry: ®istry.Registry{}, + registry: ®types.Registry{}, image: "docker.io/toolhive/test:v1.0.0", expected: false, }, diff --git a/cmd/thv-proxyrunner/app/run.go b/cmd/thv-proxyrunner/app/run.go index 1eaeee433..ebc4076bd 100644 --- a/cmd/thv-proxyrunner/app/run.go +++ b/cmd/thv-proxyrunner/app/run.go @@ -10,7 +10,7 @@ import ( "github.com/stacklok/toolhive/pkg/container" "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/workloads" ) @@ -88,7 +88,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { // we use the DetachedEnvVarValidator. envVarValidator := &runner.DetachedEnvVarValidator{} - var imageMetadata *registry.ImageMetadata + var imageMetadata *regtypes.ImageMetadata // Get the name of the MCP server to run. // This may be a server name from the registry, a container image, or a protocol scheme. diff --git a/cmd/thv/app/run.go b/cmd/thv/app/run.go index 81502080e..4b8325daf 100644 --- a/cmd/thv/app/run.go +++ b/cmd/thv/app/run.go @@ -259,7 +259,7 @@ func deriveRemoteName(remoteURL string) (string, error) { // getworkloadDefaultName generates a default workload name based on the serverOrImage input // This function reuses the existing system's naming logic to ensure consistency -func getworkloadDefaultName(ctx context.Context, serverOrImage string) string { +func getworkloadDefaultName(_ context.Context, serverOrImage string) string { // If it's a protocol scheme (uvx://, npx://, go://) if runner.IsImageProtocolScheme(serverOrImage) { // Extract package name from protocol scheme using the existing parseProtocolScheme logic diff --git a/pkg/api/v1/registry.go b/pkg/api/v1/registry.go index 614ca7746..faa42e139 100644 --- a/pkg/api/v1/registry.go +++ b/pkg/api/v1/registry.go @@ -243,69 +243,16 @@ func (rr *RegistryRoutes) updateRegistry(w http.ResponseWriter, r *http.Request) } // Validate that only one of URL, APIURL, or LocalPath is provided - if (req.URL != nil && req.APIURL != nil) || - (req.URL != nil && req.LocalPath != nil) || - (req.APIURL != nil && req.LocalPath != nil) { - http.Error(w, "Cannot specify more than one registry type (url, api_url, or local_path)", http.StatusBadRequest) + if err := validateRegistryRequest(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } - var responseType string - var message string - - // Handle reset to default (no URL, APIURL, or LocalPath specified) - if req.URL == nil && req.APIURL == nil && req.LocalPath == nil { - // Use the config provider to unset the registry - provider := rr.configProvider - if err := provider.UnsetRegistry(); err != nil { - logger.Errorf("Failed to unset registry: %v", err) - http.Error(w, "Failed to reset registry configuration", http.StatusInternalServerError) - return - } - responseType = "default" - message = "Registry configuration reset to default" - } else if req.URL != nil { - // Handle URL update - allowPrivateIP := false - if req.AllowPrivateIP != nil { - allowPrivateIP = *req.AllowPrivateIP - } - - // Use the config provider to update the registry URL - if err := rr.configProvider.SetRegistryURL(*req.URL, allowPrivateIP); err != nil { - logger.Errorf("Failed to set registry URL: %v", err) - http.Error(w, fmt.Sprintf("Failed to set registry URL: %v", err), http.StatusBadRequest) - return - } - responseType = "url" - message = fmt.Sprintf("Successfully set registry URL: %s", *req.URL) - } else if req.APIURL != nil { - // Handle API URL update - allowPrivateIP := false - if req.AllowPrivateIP != nil { - allowPrivateIP = *req.AllowPrivateIP - } - - // Use the config provider to update the registry API URL - if err := rr.configProvider.SetRegistryAPI(*req.APIURL, allowPrivateIP); err != nil { - logger.Errorf("Failed to set registry API URL: %v", err) - http.Error(w, fmt.Sprintf("Failed to set registry API URL: %v", err), http.StatusBadRequest) - return - } - responseType = "api" - message = fmt.Sprintf("Successfully set registry API URL: %s", *req.APIURL) - } else if req.LocalPath != nil { - // Handle local path update - // Use the config provider to update the registry file - provider := rr.configProvider - - if err := provider.SetRegistryFile(*req.LocalPath); err != nil { - logger.Errorf("Failed to set registry file: %v", err) - http.Error(w, fmt.Sprintf("Failed to set registry file: %v", err), http.StatusBadRequest) - return - } - responseType = "file" - message = fmt.Sprintf("Successfully set local registry file: %s", *req.LocalPath) + // Process the registry update + responseType, message, err := rr.processRegistryUpdate(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } // Reset the default provider to pick up configuration changes @@ -324,6 +271,79 @@ func (rr *RegistryRoutes) updateRegistry(w http.ResponseWriter, r *http.Request) } } +// validateRegistryRequest validates that only one registry type is specified +func validateRegistryRequest(req *UpdateRegistryRequest) error { + if (req.URL != nil && req.APIURL != nil) || + (req.URL != nil && req.LocalPath != nil) || + (req.APIURL != nil && req.LocalPath != nil) { + return fmt.Errorf("cannot specify more than one registry type (url, api_url, or local_path)") + } + return nil +} + +// processRegistryUpdate processes the registry update based on request type +func (rr *RegistryRoutes) processRegistryUpdate(req *UpdateRegistryRequest) (string, string, error) { + if req.URL == nil && req.APIURL == nil && req.LocalPath == nil { + return rr.handleRegistryReset() + } + if req.URL != nil { + return rr.handleRegistryURL(*req.URL, req.AllowPrivateIP) + } + if req.APIURL != nil { + return rr.handleRegistryAPIURL(*req.APIURL, req.AllowPrivateIP) + } + if req.LocalPath != nil { + return rr.handleRegistryLocalPath(*req.LocalPath) + } + return "", "", fmt.Errorf("no valid registry configuration provided") +} + +// handleRegistryReset resets the registry to default +func (rr *RegistryRoutes) handleRegistryReset() (string, string, error) { + if err := rr.configProvider.UnsetRegistry(); err != nil { + logger.Errorf("Failed to unset registry: %v", err) + return "", "", fmt.Errorf("failed to reset registry configuration") + } + return "default", "Registry configuration reset to default", nil +} + +// handleRegistryURL updates the registry URL +func (rr *RegistryRoutes) handleRegistryURL(url string, allowPrivateIP *bool) (string, string, error) { + allow := false + if allowPrivateIP != nil { + allow = *allowPrivateIP + } + + if err := rr.configProvider.SetRegistryURL(url, allow); err != nil { + logger.Errorf("Failed to set registry URL: %v", err) + return "", "", fmt.Errorf("failed to set registry URL: %v", err) + } + return "url", fmt.Sprintf("Successfully set registry URL: %s", url), nil +} + +// handleRegistryAPIURL updates the registry API URL +func (rr *RegistryRoutes) handleRegistryAPIURL(apiURL string, allowPrivateIP *bool) (string, string, error) { + allow := false + if allowPrivateIP != nil { + allow = *allowPrivateIP + } + + if err := rr.configProvider.SetRegistryAPI(apiURL, allow); err != nil { + logger.Errorf("Failed to set registry API URL: %v", err) + return "", "", fmt.Errorf("failed to set registry API URL: %v", err) + } + return "api", fmt.Sprintf("Successfully set registry API URL: %s", apiURL), nil +} + +// handleRegistryLocalPath updates the registry local path +func (rr *RegistryRoutes) handleRegistryLocalPath(localPath string) (string, string, error) { + if err := rr.configProvider.SetRegistryFile(localPath); err != nil { + logger.Errorf("Failed to set registry file: %v", err) + return "", "", fmt.Errorf("failed to set registry file: %v", err) + } + return "file", fmt.Sprintf("Successfully set local registry file: %s", localPath), nil +} + // removeRegistry // // @Summary Remove a registry diff --git a/pkg/api/v1/workload_types.go b/pkg/api/v1/workload_types.go index 338e0b34f..fbbfbdab1 100644 --- a/pkg/api/v1/workload_types.go +++ b/pkg/api/v1/workload_types.go @@ -69,9 +69,9 @@ type updateRequest struct { Group string `json:"group,omitempty"` // Remote server specific fields - URL string `json:"url,omitempty"` - OAuthConfig remoteOAuthConfig `json:"oauth_config,omitempty"` - Headers []*types.Header `json:"headers,omitempty"` + URL string `json:"url,omitempty"` + OAuthConfig remoteOAuthConfig `json:"oauth_config,omitempty"` + Headers []*types.Header `json:"headers,omitempty"` } // toolOverride represents a tool override diff --git a/pkg/api/v1/workloads_test.go b/pkg/api/v1/workloads_test.go index ab7b86ebb..8054bf6a9 100644 --- a/pkg/api/v1/workloads_test.go +++ b/pkg/api/v1/workloads_test.go @@ -19,7 +19,7 @@ import ( "github.com/stacklok/toolhive/pkg/core" groupsmocks "github.com/stacklok/toolhive/pkg/groups/mocks" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/runner/retriever" workloadsmocks "github.com/stacklok/toolhive/pkg/workloads/mocks" @@ -210,7 +210,7 @@ func TestCreateWorkload(t *testing.T) { mockRetriever := makeMockRetriever(t, "test-image", "test-image", - &types.ImageMetadata{Image: "test-image"}, + ®types.ImageMetadata{Image: "test-image"}, nil, ) @@ -403,7 +403,7 @@ func TestUpdateWorkload(t *testing.T) { mockRetriever := makeMockRetriever(t, "test-image", "test-image", - &types.ImageMetadata{Image: "test-image"}, + ®types.ImageMetadata{Image: "test-image"}, nil, ) @@ -439,12 +439,12 @@ func makeMockRetriever( t *testing.T, expectedServerOrImage string, returnedImage string, - returnedServerMetadata registry.ServerMetadata, + returnedServerMetadata regtypes.ServerMetadata, returnedError error, ) retriever.Retriever { t.Helper() - return func(_ context.Context, serverOrImage string, _ string, verificationType string, _ string) (string, registry.ServerMetadata, error) { + return func(_ context.Context, serverOrImage string, _ string, verificationType string, _ string) (string, regtypes.ServerMetadata, error) { assert.Equal(t, expectedServerOrImage, serverOrImage) assert.Equal(t, retriever.VerifyImageWarn, verificationType) return returnedImage, returnedServerMetadata, returnedError diff --git a/pkg/auth/remote/config.go b/pkg/auth/remote/config.go index 5b9b85fe5..8f321873f 100644 --- a/pkg/auth/remote/config.go +++ b/pkg/auth/remote/config.go @@ -56,20 +56,20 @@ func (r *Config) UnmarshalJSON(data []byte) error { if _, isOld := raw["ClientID"]; isOld { // Unmarshal using old PascalCase format var oldFormat struct { - ClientID string `json:"ClientID,omitempty"` - ClientSecret string `json:"ClientSecret,omitempty"` - ClientSecretFile string `json:"ClientSecretFile,omitempty"` - Scopes []string `json:"Scopes,omitempty"` - SkipBrowser bool `json:"SkipBrowser,omitempty"` - Timeout time.Duration `json:"Timeout,omitempty"` - CallbackPort int `json:"CallbackPort,omitempty"` - UsePKCE bool `json:"UsePKCE,omitempty"` - Issuer string `json:"Issuer,omitempty"` - AuthorizeURL string `json:"AuthorizeURL,omitempty"` - TokenURL string `json:"TokenURL,omitempty"` - Headers []*types.Header `json:"Headers,omitempty"` - EnvVars []*types.EnvVar `json:"EnvVars,omitempty"` - OAuthParams map[string]string `json:"OAuthParams,omitempty"` + ClientID string `json:"ClientID,omitempty"` + ClientSecret string `json:"ClientSecret,omitempty"` + ClientSecretFile string `json:"ClientSecretFile,omitempty"` + Scopes []string `json:"Scopes,omitempty"` + SkipBrowser bool `json:"SkipBrowser,omitempty"` + Timeout time.Duration `json:"Timeout,omitempty"` + CallbackPort int `json:"CallbackPort,omitempty"` + UsePKCE bool `json:"UsePKCE,omitempty"` + Issuer string `json:"Issuer,omitempty"` + AuthorizeURL string `json:"AuthorizeURL,omitempty"` + TokenURL string `json:"TokenURL,omitempty"` + Headers []*types.Header `json:"Headers,omitempty"` + EnvVars []*types.EnvVar `json:"EnvVars,omitempty"` + OAuthParams map[string]string `json:"OAuthParams,omitempty"` } if err := json.Unmarshal(data, &oldFormat); err != nil { diff --git a/pkg/mcp/server/handler_mock_test.go b/pkg/mcp/server/handler_mock_test.go index ba031b6d0..5c9213ae2 100644 --- a/pkg/mcp/server/handler_mock_test.go +++ b/pkg/mcp/server/handler_mock_test.go @@ -11,8 +11,8 @@ import ( runtime "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/core" - "github.com/stacklok/toolhive/pkg/registry" registrymocks "github.com/stacklok/toolhive/pkg/registry/mocks" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" workloadsmocks "github.com/stacklok/toolhive/pkg/workloads/mocks" ) @@ -24,7 +24,7 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { tests := []struct { name string query string - mockServers []registry.ServerMetadata + mockServers []regtypes.ServerMetadata setupMocks func(*registrymocks.MockProvider) wantErr bool checkResult func(*testing.T, *mcp.CallToolResult) @@ -32,9 +32,9 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { { name: "successful search with results", query: "test", - mockServers: []registry.ServerMetadata{ - &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + mockServers: []regtypes.ServerMetadata{ + ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-server", Description: "Test server description", Transport: "sse", @@ -43,8 +43,8 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { }, Image: "test/image:latest", }, - &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "another-test", Description: "Another test server", Transport: "stdio", @@ -55,9 +55,9 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { setupMocks: func(m *registrymocks.MockProvider) { m.EXPECT(). SearchServers("test"). - Return([]registry.ServerMetadata{ - &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + Return([]regtypes.ServerMetadata{ + ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-server", Description: "Test server description", Transport: "sse", @@ -66,8 +66,8 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { }, Image: "test/image:latest", }, - &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "another-test", Description: "Another test server", Transport: "stdio", @@ -86,11 +86,11 @@ func TestHandler_SearchRegistry_WithMocks(t *testing.T) { { name: "empty search results", query: "nonexistent", - mockServers: []registry.ServerMetadata{}, + mockServers: []regtypes.ServerMetadata{}, setupMocks: func(m *registrymocks.MockProvider) { m.EXPECT(). SearchServers("nonexistent"). - Return([]registry.ServerMetadata{}, nil) + Return([]regtypes.ServerMetadata{}, nil) }, wantErr: false, checkResult: func(t *testing.T, result *mcp.CallToolResult) { diff --git a/pkg/mcp/server/handler_test.go b/pkg/mcp/server/handler_test.go index 327898365..6388b6777 100644 --- a/pkg/mcp/server/handler_test.go +++ b/pkg/mcp/server/handler_test.go @@ -7,7 +7,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" - "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/runner" ) @@ -137,7 +137,7 @@ func TestConfigureTransport(t *testing.T) { t.Parallel() tests := []struct { name string - imageMetadata *types.ImageMetadata + imageMetadata *regtypes.ImageMetadata expectedTransport string }{ { @@ -147,8 +147,8 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with empty transport returns SSE", - imageMetadata: &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + imageMetadata: ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "", }, }, @@ -156,8 +156,8 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with stdio transport", - imageMetadata: &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + imageMetadata: ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "stdio", }, }, @@ -165,8 +165,8 @@ func TestConfigureTransport(t *testing.T) { }, { name: "metadata with streamable-http transport", - imageMetadata: &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + imageMetadata: ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "streamable-http", }, }, @@ -189,7 +189,7 @@ func TestPrepareEnvironmentVariables(t *testing.T) { t.Parallel() tests := []struct { name string - imageMetadata *types.ImageMetadata + imageMetadata *regtypes.ImageMetadata userEnv map[string]string expected map[string]string }{ @@ -201,8 +201,8 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with defaults, no user env", - imageMetadata: &types.ImageMetadata{ - EnvVars: []*registry.EnvVar{ + imageMetadata: ®types.ImageMetadata{ + EnvVars: []*regtypes.EnvVar{ {Name: "VAR1", Default: "default1"}, {Name: "VAR2", Default: "default2"}, }, @@ -215,8 +215,8 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with defaults, user overrides", - imageMetadata: &types.ImageMetadata{ - EnvVars: []*registry.EnvVar{ + imageMetadata: ®types.ImageMetadata{ + EnvVars: []*regtypes.EnvVar{ {Name: "VAR1", Default: "default1"}, {Name: "VAR2", Default: "default2"}, }, @@ -243,8 +243,8 @@ func TestPrepareEnvironmentVariables(t *testing.T) { }, { name: "metadata with empty defaults ignored", - imageMetadata: &types.ImageMetadata{ - EnvVars: []*registry.EnvVar{ + imageMetadata: ®types.ImageMetadata{ + EnvVars: []*regtypes.EnvVar{ {Name: "VAR1", Default: ""}, {Name: "VAR2", Default: "value2"}, }, @@ -281,7 +281,7 @@ func TestBuildServerConfig(t *testing.T) { tests := []struct { name string imageURL string - imageMetadata *types.ImageMetadata + imageMetadata *regtypes.ImageMetadata expectError bool }{ { @@ -293,13 +293,13 @@ func TestBuildServerConfig(t *testing.T) { { name: "valid config with metadata", imageURL: "test/image:latest", - imageMetadata: &types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + imageMetadata: ®types.ImageMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "stdio", }, Image: "test/image:latest", Args: []string{"--test"}, - EnvVars: []*registry.EnvVar{ + EnvVars: []*regtypes.EnvVar{ {Name: "DEFAULT_VAR", Default: "default_value"}, }, }, diff --git a/pkg/registry/converters/converters_fixture_test.go b/pkg/registry/converters/converters_fixture_test.go index 0ccf5c4db..da982e152 100644 --- a/pkg/registry/converters/converters_fixture_test.go +++ b/pkg/registry/converters/converters_fixture_test.go @@ -7,9 +7,10 @@ import ( "testing" upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" ) // TestConverters_Fixtures validates converter functions using JSON fixture files diff --git a/pkg/registry/converters/converters_test.go b/pkg/registry/converters/converters_test.go index dfa66932c..98195647e 100644 --- a/pkg/registry/converters/converters_test.go +++ b/pkg/registry/converters/converters_test.go @@ -6,9 +6,10 @@ import ( upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" - "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" ) // Test Helpers diff --git a/pkg/registry/converters/integration_test.go b/pkg/registry/converters/integration_test.go index aab5328a9..e326c218f 100644 --- a/pkg/registry/converters/integration_test.go +++ b/pkg/registry/converters/integration_test.go @@ -9,8 +9,9 @@ import ( "testing" upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" ) // ToolHiveRegistry represents the structure of registry.json diff --git a/pkg/registry/converters/toolhive_to_upstream.go b/pkg/registry/converters/toolhive_to_upstream.go index d920cecc5..1b064abd0 100644 --- a/pkg/registry/converters/toolhive_to_upstream.go +++ b/pkg/registry/converters/toolhive_to_upstream.go @@ -15,6 +15,7 @@ import ( upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stacklok/toolhive/pkg/registry/types" ) diff --git a/pkg/registry/converters/upstream_to_toolhive.go b/pkg/registry/converters/upstream_to_toolhive.go index ce72a4d3f..676fcb289 100644 --- a/pkg/registry/converters/upstream_to_toolhive.go +++ b/pkg/registry/converters/upstream_to_toolhive.go @@ -10,6 +10,7 @@ import ( upstream "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stacklok/toolhive/pkg/permissions" "github.com/stacklok/toolhive/pkg/registry/types" ) diff --git a/pkg/registry/provider_test.go b/pkg/registry/provider_test.go index f95779917..04e73155c 100644 --- a/pkg/registry/provider_test.go +++ b/pkg/registry/provider_test.go @@ -1,8 +1,6 @@ package registry import ( - - "github.com/stacklok/toolhive/pkg/registry/types" "os" "path/filepath" "testing" @@ -11,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stacklok/toolhive/pkg/config" + "github.com/stacklok/toolhive/pkg/registry/types" ) func TestNewRegistryProvider(t *testing.T) { diff --git a/pkg/registry/schema_validation_test.go b/pkg/registry/schema_validation_test.go index 9b3636190..d52ea1c18 100644 --- a/pkg/registry/schema_validation_test.go +++ b/pkg/registry/schema_validation_test.go @@ -1,13 +1,13 @@ package registry import ( - - "github.com/stacklok/toolhive/pkg/registry/types" "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" ) // TestEmbeddedRegistrySchemaValidation validates that the embedded registry.json diff --git a/pkg/registry/types_test.go b/pkg/registry/types_test.go index 0430da5de..f1ed5cd46 100644 --- a/pkg/registry/types_test.go +++ b/pkg/registry/types_test.go @@ -1,14 +1,14 @@ package registry import ( - - "github.com/stacklok/toolhive/pkg/registry/types" "encoding/json" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" ) func TestRegistryWithRemoteServers(t *testing.T) { diff --git a/pkg/registry/upstream_conversion.go b/pkg/registry/upstream_conversion.go index 166b478e7..0c0e41903 100644 --- a/pkg/registry/upstream_conversion.go +++ b/pkg/registry/upstream_conversion.go @@ -86,7 +86,9 @@ func convertToRemoteServer(upstream *UpstreamServerDetail, toolhiveExt *Toolhive } // convertToImageMetadata converts upstream format to ImageMetadata -func convertToImageMetadata(upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension) (*types.ImageMetadata, error) { +func convertToImageMetadata( + upstream *UpstreamServerDetail, toolhiveExt *ToolhivePublisherExtension, +) (*types.ImageMetadata, error) { if len(upstream.Server.Packages) == 0 { return nil, fmt.Errorf("no packages found for container server") } diff --git a/pkg/registry/upstream_conversion_test.go b/pkg/registry/upstream_conversion_test.go index 4d7035d12..ef2bde33d 100644 --- a/pkg/registry/upstream_conversion_test.go +++ b/pkg/registry/upstream_conversion_test.go @@ -1,14 +1,13 @@ package registry import ( - - "github.com/stacklok/toolhive/pkg/registry/types" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stacklok/toolhive/pkg/permissions" + "github.com/stacklok/toolhive/pkg/registry/types" ) func TestConvertUpstreamToToolhive_DockerPackage(t *testing.T) { diff --git a/pkg/runner/config_builder_test.go b/pkg/runner/config_builder_test.go index 8a066b916..3a7490390 100644 --- a/pkg/runner/config_builder_test.go +++ b/pkg/runner/config_builder_test.go @@ -14,7 +14,6 @@ import ( "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/mcp" "github.com/stacklok/toolhive/pkg/permissions" - "github.com/stacklok/toolhive/pkg/registry" regtypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/transport/types" ) @@ -35,7 +34,7 @@ func TestRunConfigBuilder_Build_WithPermissionProfile(t *testing.T) { }` imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", }, Permissions: &permissions.Profile{ @@ -557,7 +556,7 @@ func TestRunConfigBuilder_ToolOverrideMutualExclusivity(t *testing.T) { mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, @@ -636,7 +635,7 @@ func TestRunConfigBuilder_ToolOverrideWithToolsFilter(t *testing.T) { mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, @@ -700,7 +699,7 @@ func TestNewOperatorRunConfigBuilder(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, @@ -769,7 +768,7 @@ func TestWithEnvVars(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, @@ -797,7 +796,7 @@ func TestWithEnvVarsOverwrite(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, @@ -882,7 +881,7 @@ func TestBuildForOperator(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-image", Tools: []string{"tool1", "tool2", "tool3"}, }, diff --git a/pkg/runner/config_test.go b/pkg/runner/config_test.go index 8361d9015..3490a307b 100644 --- a/pkg/runner/config_test.go +++ b/pkg/runner/config_test.go @@ -17,7 +17,6 @@ import ( "github.com/stacklok/toolhive/pkg/ignore" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/permissions" - "github.com/stacklok/toolhive/pkg/registry" regtypes "github.com/stacklok/toolhive/pkg/registry/types" secretsmocks "github.com/stacklok/toolhive/pkg/secrets/mocks" "github.com/stacklok/toolhive/pkg/telemetry" @@ -567,7 +566,7 @@ func TestRunConfigBuilder(t *testing.T) { name := "test-server" imageURL := "test-image:latest" imageMetadata := ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Name: "test-metadata-name", Transport: "sse", }, @@ -790,7 +789,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { userTransport: "", userTargetPort: 0, metadata: ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "streamable-http", }, TargetPort: 3000, @@ -803,7 +802,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { userTransport: "stdio", userTargetPort: 0, metadata: ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "sse", }, TargetPort: 3000, @@ -816,7 +815,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) { userTransport: "sse", userTargetPort: 4000, metadata: ®types.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: regtypes.BaseServerMetadata{ Transport: "sse", }, TargetPort: 3000, diff --git a/pkg/runner/retriever/retriever_test.go b/pkg/runner/retriever/retriever_test.go index 62bef9f99..b9382d74a 100644 --- a/pkg/runner/retriever/retriever_test.go +++ b/pkg/runner/retriever/retriever_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stacklok/toolhive/pkg/registry" + regtypes "github.com/stacklok/toolhive/pkg/registry/types" ) func TestGetMCPServer_WithGroup(t *testing.T) { @@ -24,7 +25,7 @@ func TestGetMCPServer_WithGroup(t *testing.T) { // Find a group that exists in the registry var testGroupName string - var group *registry.Group + var group *regtypes.Group for _, g := range reg.Groups { if g != nil && g.Name != "" { testGroupName = g.Name From fbe5b3e7c4493bfeae10cf780847c7a03d74a4ab Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 6 Nov 2025 10:06:52 +0200 Subject: [PATCH 12/20] Fix CodeQL warning and regenerate swagger docs Signed-off-by: Radoslav Dimitrov --- docs/cli/thv_config.md | 1 + docs/cli/thv_config_set-registry-api.md | 48 ++ docs/server/docs.go | 6 +- docs/server/swagger.json | 6 +- docs/server/swagger.yaml | 1002 +++++++++++------------ pkg/config/registry.go | 9 +- 6 files changed, 563 insertions(+), 509 deletions(-) create mode 100644 docs/cli/thv_config_set-registry-api.md diff --git a/docs/cli/thv_config.md b/docs/cli/thv_config.md index 8a503af4a..5f256b52c 100644 --- a/docs/cli/thv_config.md +++ b/docs/cli/thv_config.md @@ -37,6 +37,7 @@ The config command provides subcommands to manage application configuration sett * [thv config otel](thv_config_otel.md) - Manage OpenTelemetry configuration * [thv config set-ca-cert](thv_config_set-ca-cert.md) - Set the default CA certificate for container builds * [thv config set-registry](thv_config_set-registry.md) - Set the MCP server registry +* [thv config set-registry-api](thv_config_set-registry-api.md) - Set the MCP Registry API endpoint * [thv config unset-ca-cert](thv_config_unset-ca-cert.md) - Remove the configured CA certificate * [thv config unset-registry](thv_config_unset-registry.md) - Remove the configured registry * [thv config usage-metrics](thv_config_usage-metrics.md) - Enable or disable anonymous usage metrics diff --git a/docs/cli/thv_config_set-registry-api.md b/docs/cli/thv_config_set-registry-api.md new file mode 100644 index 000000000..f35508314 --- /dev/null +++ b/docs/cli/thv_config_set-registry-api.md @@ -0,0 +1,48 @@ +--- +title: thv config set-registry-api +hide_title: true +description: Reference for ToolHive CLI command `thv config set-registry-api` +last_update: + author: autogenerated +slug: thv_config_set-registry-api +mdx: + format: md +--- + +## thv config set-registry-api + +Set the MCP Registry API endpoint + +### Synopsis + +Set the MCP Registry API endpoint that implements the MCP Registry API v0.1 specification. +This enables on-demand querying of servers from a live registry API. + +The API endpoint must implement the official MCP Registry API specification from +https://registry.modelcontextprotocol.io/docs + +Examples: + thv config set-registry-api https://registry.example.com # API endpoint + thv config set-registry-api https://api.example.com --allow-private-ip # With private IP support + +``` +thv config set-registry-api [flags] +``` + +### Options + +``` + -p, --allow-private-ip Allow setting the registry API URL, even if it references a private IP address + -h, --help help for set-registry-api +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/server/docs.go b/docs/server/docs.go index 51a98e7e5..2c6491042 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -6,7 +6,11 @@ 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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707).","type":"string"},"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"},"resource":{"description":"OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"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"}}}, +<<<<<<< HEAD + "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"}}}, +======= + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, +>>>>>>> e59f7fb6 (Fix CodeQL warning and regenerate swagger docs) "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 199095d83..426b1ee9d 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -1,5 +1,9 @@ { - "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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707).","type":"string"},"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"},"resource":{"description":"OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"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"}}}, +<<<<<<< HEAD + "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"}}}, +======= + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, +>>>>>>> e59f7fb6 (Fix CodeQL warning and regenerate swagger docs) "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 dae27248d..9f6ef9eb6 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -333,289 +333,392 @@ components: type: array uniqueItems: false type: object - registry.EnvVar: + remote.Config: + description: RemoteAuthConfig contains OAuth configuration for remote MCP servers properties: - default: - description: |- - Default is the value to use if the environment variable is not explicitly provided - Only used for non-required variables + authorize_url: type: string - description: - description: Description is a human-readable explanation of the variable's - purpose + callback_port: + type: integer + client_id: type: string - name: - description: Name is the environment variable name (e.g., API_KEY) + client_secret: type: string - required: - description: |- - Required indicates whether this environment variable must be provided - If 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 - If 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 + client_secret_file: type: string - name: - description: Name is the identifier for the group, used when referencing - the group in commands + env_vars: + description: Environment variables for the client + items: + $ref: '#/components/schemas/types.EnvVar' + type: array + uniqueItems: false + headers: + description: Headers for HTTP requests + items: + $ref: '#/components/schemas/types.Header' + type: array + uniqueItems: false + issuer: + description: OAuth endpoint configuration (from registry) 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: + oauth_params: additionalProperties: - $ref: '#/components/schemas/registry.ImageMetadata' - description: Servers is a map of server names to their corresponding server - definitions within this group + type: string + description: OAuth parameters for server-specific customization type: object - type: object - registry.Header: - properties: - choices: - description: Choices provides a list of valid values for the header (optional) + scopes: items: type: string type: array uniqueItems: false - default: - description: |- - Default is the value to use if the header is not explicitly provided - Only used for non-required headers - type: string - description: - description: Description is a human-readable explanation of the header's - purpose + skip_browser: + type: boolean + timeout: + example: 5m type: string - name: - description: Name is the header name (e.g., X-API-Key, Authorization) + token_url: type: string - required: - description: |- - Required indicates whether this header must be provided - If 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 - If true, the value will be stored as a secret rather than as plain text + use_pkce: type: boolean type: object - registry.ImageMetadata: - description: Container server details (if it's a container server) + runner.RunConfig: properties: - args: - description: |- - Args are the default command-line arguments to pass to the MCP server container. - These arguments will be used only if no command-line arguments are provided by the user. - If the user provides arguments, they will override these defaults. + 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 - custom_metadata: - additionalProperties: {} - description: CustomMetadata allows for additional user-defined metadata + container_labels: + additionalProperties: + type: string + description: ContainerLabels are the labels to apply to the container type: object - description: - description: Description is a human-readable description of the server's - purpose and functionality + 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 - 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 + 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 + Only applicable when using Kubernetes runtime + type: string + middleware_configs: + description: |- + MiddlewareConfigs contains the list of middleware to apply to the transport + and the configuration for each middleware. items: - $ref: '#/components/schemas/registry.EnvVar' + $ref: '#/components/schemas/types.MiddlewareConfig' 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 - If not provided, it will be auto-generated from the registry key + description: Name is the name of the MCP server type: string - permissions: + oidc_config: + $ref: '#/components/schemas/auth.TokenValidatorConfig' + permission_profile: $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 + permission_profile_name_or_path: + description: PermissionProfileNameOrPath is the name or path of the permission + profile type: string - status: - description: Status indicates whether the server is currently active or - deprecated + 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 - tags: - description: Tags are categorization labels for the server to aid in discovery - and filtering + 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 + Format: ",target=" 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 and Streamable HTTP transports) + to SSE transport) type: integer - tier: - description: Tier represents the tier classification level of the server, - e.g., "Official" or "Community" + 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: - description: Tools is a list of tool names provided by this MCP server + 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 defines the communication protocol for the server - For containers: stdio, sse, or streamable-http - For remote servers: sse or streamable-http (stdio not supported) + 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 + Format: "host-path:container-path[:ro]" + items: + type: string + type: array + uniqueItems: false type: object - registry.Metadata: - description: Metadata contains additional information about the server such - as popularity metrics + runner.ToolOverride: properties: - last_updated: - description: LastUpdated is the timestamp when the server was last updated, - in RFC3339 format + description: + description: Description is the redefined description of the tool + type: string + name: + description: Name is the redefined name of the tool 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 - Used with the thv proxy command's --remote-auth flags + 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: - authorize_url: - description: |- - AuthorizeURL is the OAuth authorization endpoint URL - Used for non-OIDC OAuth flows when issuer is not provided - type: string - callback_port: - description: |- - CallbackPort is the specific port to use for the OAuth callback server - If not specified, a random available port will be used - type: integer - client_id: - description: ClientID is the OAuth client ID for authentication + name: type: string - issuer: - description: |- - Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com) - Used for OIDC discovery to find authorization and token endpoints + target: type: string - oauth_params: + type: object + telemetry.Config: + description: TelemetryConfig contains the OpenTelemetry configuration + properties: + customAttributes: additionalProperties: type: string description: |- - OAuthParams contains additional OAuth parameters to include in the authorization request - These are server-specific parameters like "prompt", "response_mode", etc. + CustomAttributes contains custom resource attributes to be added to all telemetry signals. + These are parsed from CLI flags (--otel-custom-attributes) or environment variables + (OTEL_RESOURCE_ATTRIBUTES) as key=value pairs. + We use map[string]string for proper JSON serialization instead of []attribute.KeyValue + which doesn't marshal/unmarshal correctly. type: object - resource: - description: Resource is the OAuth 2.0 resource indicator (RFC 8707) + enablePrometheusMetricsPath: + description: |- + EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint + The metrics are served on the main transport port at /metrics + This is separate from OTLP metrics which are sent to the Endpoint + type: boolean + endpoint: + description: Endpoint is the OTLP endpoint URL type: string - scopes: + environmentVariables: description: |- - Scopes are the OAuth scopes to request - If not specified, defaults to ["openid", "profile", "email"] for OIDC + EnvironmentVariables is a list of environment variable names that should be + included in telemetry spans as attributes. Only variables in this list will + be read from the host machine and included in spans for observability. + Example: []string{"NODE_ENV", "DEPLOYMENT_ENV", "SERVICE_VERSION"} items: type: string type: array uniqueItems: false - token_url: + 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: |- - TokenURL is the OAuth token endpoint URL - Used for non-OIDC OAuth flows when issuer is not provided + MetricsEnabled controls whether OTLP metrics are enabled + When false, OTLP metrics are not sent even if an endpoint is configured + This is independent of EnablePrometheusMetricsPath + type: boolean + samplingRate: + description: |- + SamplingRate is the trace sampling rate (0.0-1.0) + Only used when TracingEnabled is true + type: number + serviceName: + description: ServiceName is the service name for telemetry type: string - use_pkce: + serviceVersion: + description: ServiceVersion is the service version for telemetry + type: string + tracingEnabled: description: |- - UsePKCE indicates whether to use PKCE for the OAuth flow - Defaults to true for enhanced security + TracingEnabled controls whether distributed tracing is enabled + When false, no tracer provider is created even if an endpoint is configured type: boolean type: object - registry.Provenance: - description: Provenance contains verification and signing metadata + types.EnvVar: properties: - attestation: - $ref: '#/components/schemas/registry.VerifiedAttestation' - cert_issuer: - type: string - repository_ref: - type: string - repository_uri: - type: string - runner_environment: + default: + description: |- + Default is the value to use if the environment variable is not explicitly provided + Only used for non-required variables type: string - signer_identity: + description: + description: Description is a human-readable explanation of the variable's + purpose type: string - sigstore_url: + name: + description: Name is the environment variable name (e.g., API_KEY) 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 + required: + description: |- + Required indicates whether this environment variable must be provided + If 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 + If true, the value will be stored as a secret rather than as a plain environment variable + type: boolean + type: object + types.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 - These are MCP servers accessed via HTTP/HTTPS using the thv proxy command + $ref: '#/components/schemas/types.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' + $ref: '#/components/schemas/types.ImageMetadata' description: Servers is a map of server names to their corresponding server - definitions + definitions within this group type: object - version: - description: Version is the schema version of the registry + type: object + types.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 + Only 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 + If 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 + If true, the value will be stored as a secret rather than as plain text + type: boolean type: object - registry.RemoteServerMetadata: - description: Remote server details (if it's a remote server) + types.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. + These arguments will be used only if no command-line arguments are provided by the user. + If 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 @@ -624,31 +727,34 @@ components: 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 - These might be needed for client-side configuration when connecting to the remote server + docker_tags: + description: DockerTags lists the available Docker tags for this server + image items: - $ref: '#/components/schemas/registry.EnvVar' + type: string type: array uniqueItems: false - headers: - description: |- - Headers defines HTTP headers that can be passed to the remote server for authentication - These are used with the thv proxy command's authentication features + env_vars: + description: EnvVars defines environment variables that can be passed to + the server items: - $ref: '#/components/schemas/registry.Header' + $ref: '#/components/schemas/types.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' + $ref: '#/components/schemas/types.Metadata' name: description: |- Name is the identifier for the MCP server, used when referencing the server in commands If not provided, it will be auto-generated from the registry key type: string - oauth_config: - $ref: '#/components/schemas/registry.OAuthConfig' + permissions: + $ref: '#/components/schemas/permissions.Profile' + provenance: + $ref: '#/components/schemas/types.Provenance' repository_url: description: RepositoryURL is the URL to the source code repository for the server @@ -664,6 +770,10 @@ components: 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" @@ -680,331 +790,209 @@ components: For containers: stdio, sse, or streamable-http For 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: + types.Metadata: + description: Metadata contains additional information about the server such + as popularity metrics properties: - predicate: {} - predicate_type: + 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 - remote.Config: - description: RemoteAuthConfig contains OAuth configuration for remote MCP servers + types.MiddlewareConfig: 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 + parameters: + description: |- + Parameters is a JSON object containing the middleware parameters. + It is stored as a raw message to allow flexible parameter types. type: object - resource: - description: Resource is the OAuth 2.0 resource indicator (RFC 8707). - type: string - scopes: - items: - type: string - type: array - uniqueItems: false - skip_browser: - type: boolean - timeout: - example: 5m - type: string - token_url: + type: + description: Type is a string representing the middleware type. type: string - use_pkce: - type: boolean type: object - runner.RunConfig: + types.OAuthConfig: + description: |- + OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server + Used with the thv proxy command's --remote-auth flags 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 - Only applicable when using Kubernetes runtime - type: string - middleware_configs: - description: |- - MiddlewareConfigs contains the list of middleware to apply to the transport - and 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: + authorize_url: description: |- - Secrets are the secret parameters to pass to the container - Format: ",target=" - items: - type: string - type: array - uniqueItems: false - target_host: - description: TargetHost is the host to forward traffic to (only applicable - to SSE transport) + AuthorizeURL is the OAuth authorization endpoint URL + Used for non-OIDC OAuth flows when issuer is not provided type: string - target_port: - description: TargetPort is the port for the container to expose (only applicable - to SSE transport) + callback_port: + description: |- + CallbackPort is the specific port to use for the OAuth callback server + If not specified, a random available port will be used type: integer - telemetry_config: - $ref: '#/components/schemas/telemetry.Config' - thv_ca_bundle: - description: ThvCABundle is the path to the CA certificate bundle for ToolHive - HTTP operations + client_id: + description: ClientID is the OAuth client ID for authentication type: string - tools_filter: - description: ToolsFilter is the list of tools to filter - items: - type: string - type: array - uniqueItems: false - tools_override: + issuer: + description: |- + Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com) + Used for OIDC discovery to find authorization and token endpoints + type: string + oauth_params: additionalProperties: - $ref: '#/components/schemas/runner.ToolOverride' - description: ToolsOverride is a map from an actual tool to its overridden - name and/or description + type: string + description: |- + OAuthParams contains additional OAuth parameters to include in the authorization request + These are server-specific parameters like "prompt", "response_mode", etc. type: object - 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: + scopes: description: |- - Volumes are the directory mounts to pass to the container - Format: "host-path:container-path[:ro]" + Scopes are the OAuth scopes to request + If 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 + Used 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 + Defaults to true for enhanced security + type: boolean type: object - runner.ToolOverride: + types.Provenance: + description: Provenance contains verification and signing metadata properties: - description: - description: Description is the redefined description of the tool + attestation: + $ref: '#/components/schemas/types.VerifiedAttestation' + cert_issuer: type: string - name: - description: Name is the redefined name of the tool + repository_ref: + type: string + repository_uri: + type: string + runner_environment: + type: string + signer_identity: + type: string + sigstore_url: type: string type: object - runtime.WorkloadStatus: - description: Status is the current status of the workload. + types.ProxyMode: + description: ProxyMode is the proxy mode for stdio transport ("sse" or "streamable-http") type: string x-enum-varnames: - - WorkloadStatusRunning - - WorkloadStatusStopped - - WorkloadStatusError - - WorkloadStatusStarting - - WorkloadStatusStopping - - WorkloadStatusUnhealthy - - WorkloadStatusRemoving - - WorkloadStatusUnknown - - WorkloadStatusUnauthenticated - secrets.SecretParameter: + - ProxyModeSSE + - ProxyModeStreamableHTTP + types.Registry: + description: Full registry data properties: - name: + groups: + description: Groups is a slice of group definitions containing related MCP + servers + items: + $ref: '#/components/schemas/types.Group' + type: array + uniqueItems: false + last_updated: + description: LastUpdated is the timestamp when the registry was last updated, + in RFC3339 format type: string - target: + remote_servers: + additionalProperties: + $ref: '#/components/schemas/types.RemoteServerMetadata' + description: |- + RemoteServers is a map of server names to their corresponding remote server definitions + These are MCP servers accessed via HTTP/HTTPS using the thv proxy command + type: object + servers: + additionalProperties: + $ref: '#/components/schemas/types.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 - telemetry.Config: - description: TelemetryConfig contains the OpenTelemetry configuration + types.RemoteServerMetadata: + description: Remote server details (if it's a remote server) properties: - customAttributes: - additionalProperties: - type: string - description: |- - CustomAttributes contains custom resource attributes to be added to all telemetry signals. - These are parsed from CLI flags (--otel-custom-attributes) or environment variables - (OTEL_RESOURCE_ATTRIBUTES) as key=value pairs. - We use map[string]string for proper JSON serialization instead of []attribute.KeyValue - which doesn't marshal/unmarshal correctly. + custom_metadata: + additionalProperties: {} + description: CustomMetadata allows for additional user-defined metadata type: object - enablePrometheusMetricsPath: - description: |- - EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint - The metrics are served on the main transport port at /metrics - This is separate from OTLP metrics which are sent to the Endpoint - type: boolean - endpoint: - description: Endpoint is the OTLP endpoint URL + description: + description: Description is a human-readable description of the server's + purpose and functionality type: string - environmentVariables: + env_vars: description: |- - EnvironmentVariables is a list of environment variable names that should be - included in telemetry spans as attributes. Only variables in this list will - be read from the host machine and included in spans for observability. - Example: []string{"NODE_ENV", "DEPLOYMENT_ENV", "SERVICE_VERSION"} + EnvVars defines environment variables that can be passed to configure the client + These might be needed for client-side configuration when connecting to the remote server items: - type: string + $ref: '#/components/schemas/types.EnvVar' 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 - When false, OTLP metrics are not sent even if an endpoint is configured - This is independent of EnablePrometheusMetricsPath - type: boolean - samplingRate: + Headers defines HTTP headers that can be passed to the remote server for authentication + These are used with the thv proxy command's authentication features + items: + $ref: '#/components/schemas/types.Header' + type: array + uniqueItems: false + metadata: + $ref: '#/components/schemas/types.Metadata' + name: description: |- - SamplingRate is the trace sampling rate (0.0-1.0) - Only used when TracingEnabled is true - type: number - serviceName: - description: ServiceName is the service name for telemetry + Name is the identifier for the MCP server, used when referencing the server in commands + If not provided, it will be auto-generated from the registry key type: string - serviceVersion: - description: ServiceVersion is the service version for telemetry + oauth_config: + $ref: '#/components/schemas/types.OAuthConfig' + repository_url: + description: RepositoryURL is the URL to the source code repository for + the server type: string - tracingEnabled: - description: |- - TracingEnabled controls whether distributed tracing is enabled - When false, no tracer provider is created even if an endpoint is configured - type: boolean - type: object - types.MiddlewareConfig: - properties: - parameters: + 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: |- - Parameters is a JSON object containing the middleware parameters. - It is stored as a raw message to allow flexible parameter types. - type: object - type: - description: Type is a string representing the middleware type. + Transport defines the communication protocol for the server + For containers: stdio, sse, or streamable-http + For 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 - 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 @@ -1013,19 +1001,29 @@ components: - TransportTypeSSE - TransportTypeStreamableHTTP - TransportTypeInspector + types.VerifiedAttestation: + properties: + predicate: {} + predicate_type: + type: string + type: object v1.RegistryType: description: Type of registry (file, url, or default) type: string x-enum-varnames: - RegistryTypeFile - RegistryTypeURL + - RegistryTypeAPI - RegistryTypeDefault v1.UpdateRegistryRequest: description: Request containing registry configuration updates properties: allow_private_ip: - description: Allow private IP addresses for registry URL + description: Allow private IP addresses for registry URL or API URL type: boolean + api_url: + description: MCP Registry API URL + type: string local_path: description: Local registry file path type: string @@ -1190,7 +1188,7 @@ components: type: string headers: items: - $ref: '#/components/schemas/registry.Header' + $ref: '#/components/schemas/types.Header' type: array uniqueItems: false host: @@ -1294,7 +1292,7 @@ components: description: Name of the registry type: string registry: - $ref: '#/components/schemas/registry.Registry' + $ref: '#/components/schemas/types.Registry' server_count: description: Number of servers in the registry type: integer @@ -1308,6 +1306,7 @@ components: x-enum-varnames: - RegistryTypeFile - RegistryTypeURL + - RegistryTypeAPI - RegistryTypeDefault version: description: Version of the registry schema @@ -1332,9 +1331,9 @@ components: description: Indicates if this is a remote server type: boolean remote_server: - $ref: '#/components/schemas/registry.RemoteServerMetadata' + $ref: '#/components/schemas/types.RemoteServerMetadata' server: - $ref: '#/components/schemas/registry.ImageMetadata' + $ref: '#/components/schemas/types.ImageMetadata' type: object v1.groupListResponse: properties: @@ -1361,13 +1360,13 @@ components: remote_servers: description: List of remote servers in the registry (if any) items: - $ref: '#/components/schemas/registry.RemoteServerMetadata' + $ref: '#/components/schemas/types.RemoteServerMetadata' type: array uniqueItems: false servers: description: List of container servers in the registry items: - $ref: '#/components/schemas/registry.ImageMetadata' + $ref: '#/components/schemas/types.ImageMetadata' type: array uniqueItems: false type: object @@ -1467,9 +1466,6 @@ components: type: string description: Additional OAuth parameters for server-specific customization type: object - resource: - description: OAuth 2.0 resource indicator (RFC 8707) - type: string scopes: description: OAuth scopes to request items: @@ -1552,7 +1548,7 @@ components: type: string headers: items: - $ref: '#/components/schemas/registry.Header' + $ref: '#/components/schemas/types.Header' type: array uniqueItems: false host: diff --git a/pkg/config/registry.go b/pkg/config/registry.go index 175846e88..3fc14eb29 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -122,11 +122,12 @@ func setRegistryAPI(provider Provider, apiURL string, allowPrivateRegistryIp boo return fmt.Errorf("failed to create HTTP client: %w", err) } // Try to fetch the /openapi.yaml endpoint to validate - openapiURL := apiURL - if !strings.HasSuffix(apiURL, "/") { - openapiURL += "/" + // Use JoinPath for safe URL construction + openapiURL, err := neturl.JoinPath(apiURL, "openapi.yaml") + if err != nil { + return fmt.Errorf("failed to construct OpenAPI URL: %w", err) } - openapiURL += "openapi.yaml" + // #nosec G107 -- URL is validated above and path is a constant _, err = registryClient.Get(openapiURL) if err != nil && strings.Contains(fmt.Sprint(err), networking.ErrPrivateIpAddress) { return err From dbcc6c83b56338e534da82282564decad3c0f8c8 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 6 Nov 2025 10:31:07 +0200 Subject: [PATCH 13/20] Fix another CodeQL error Signed-off-by: Radoslav Dimitrov --- pkg/process/pid.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/process/pid.go b/pkg/process/pid.go index 79422302a..5f04243bb 100644 --- a/pkg/process/pid.go +++ b/pkg/process/pid.go @@ -174,7 +174,8 @@ func RemovePIDFile(containerBaseName string) error { } // Also try to remove from the old location (cleanup legacy files) - oldPath := getOldPIDFilePath(containerBaseName) + // Clean the path to satisfy security scanners (containerBaseName is already sanitized) + oldPath := filepath.Clean(getOldPIDFilePath(containerBaseName)) if err := os.Remove(oldPath); err != nil && !os.IsNotExist(err) { // If we couldn't remove either file and both had errors, return the error if lastErr != nil { From 1fe040e7b916ff31c7e120845fb2f52f050876c0 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 6 Nov 2025 12:49:28 +0200 Subject: [PATCH 14/20] Update MCP Registry API version from v0 to v0.1 in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API endpoints use v0.1 as the version path component. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/arch/06-registry-system.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/arch/06-registry-system.md b/docs/arch/06-registry-system.md index 311731f37..252e5c134 100644 --- a/docs/arch/06-registry-system.md +++ b/docs/arch/06-registry-system.md @@ -356,9 +356,9 @@ thv config unset-registry **API Requirements:** The API endpoint must implement: -- `GET /v0/servers` - List all servers with pagination -- `GET /v0/servers/:name` - Get specific server by reverse-DNS name -- `GET /v0/servers?search=` - Search servers +- `GET /v0.1/servers` - List all servers with pagination +- `GET /v0.1/servers/:name` - Get specific server by reverse-DNS name +- `GET /v0.1/servers?search=` - Search servers - `GET /openapi.yaml` - OpenAPI specification (version 1.0.0) **Response format:** From 0910e0cf1c896804faf8300b78b29a3cab51bd25 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 6 Nov 2025 13:24:13 +0200 Subject: [PATCH 15/20] Add User-Agent header to MCP Registry API client Signed-off-by: Radoslav Dimitrov --- docs/arch/06-registry-system.md | 4 ++-- pkg/registry/api/client.go | 7 +++++++ pkg/versions/version.go | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/arch/06-registry-system.md b/docs/arch/06-registry-system.md index 252e5c134..d598644b4 100644 --- a/docs/arch/06-registry-system.md +++ b/docs/arch/06-registry-system.md @@ -422,8 +422,8 @@ When multiple registries configured, ToolHive uses this priority order: The factory selects the first configured registry type in this order. To switch between registry types, use the appropriate CLI command: ```bash -# Set API registry (highest priority) -thv config set-registry-api https://registry.example.com +# Set API registry (highest priority, for example, https://registry.modelcontextprotocol.io) +thv config set-registry-api https://registry.example.com # Set remote registry (if no API registry configured) thv config set-registry https://example.com/registry.json diff --git a/pkg/registry/api/client.go b/pkg/registry/api/client.go index 2f0e66e06..8c509b43b 100644 --- a/pkg/registry/api/client.go +++ b/pkg/registry/api/client.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "github.com/stacklok/toolhive/pkg/networking" + "github.com/stacklok/toolhive/pkg/versions" ) // Client represents an MCP Registry API client @@ -48,6 +49,7 @@ type mcpRegistryClient struct { baseURL string httpClient *http.Client allowPrivateIp bool + userAgent string } // NewClient creates a new MCP Registry API client @@ -69,6 +71,7 @@ func NewClient(baseURL string, allowPrivateIp bool) (Client, error) { baseURL: baseURL, httpClient: httpClient, allowPrivateIp: allowPrivateIp, + userAgent: versions.GetUserAgent(), }, nil } @@ -83,6 +86,7 @@ func (c *mcpRegistryClient) GetServer(ctx context.Context, name string) (*v0.Ser if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("User-Agent", c.userAgent) resp, err := c.httpClient.Do(req) if err != nil { @@ -173,6 +177,7 @@ func (c *mcpRegistryClient) fetchServersPage( if err != nil { return nil, "", fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("User-Agent", c.userAgent) resp, err := c.httpClient.Do(req) if err != nil { @@ -213,6 +218,7 @@ func (c *mcpRegistryClient) SearchServers(ctx context.Context, query string) ([] if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("User-Agent", c.userAgent) resp, err := c.httpClient.Do(req) if err != nil { @@ -248,6 +254,7 @@ func (c *mcpRegistryClient) ValidateEndpoint(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("User-Agent", c.userAgent) resp, err := c.httpClient.Do(req) if err != nil { diff --git a/pkg/versions/version.go b/pkg/versions/version.go index ba2485b97..5b714e161 100644 --- a/pkg/versions/version.go +++ b/pkg/versions/version.go @@ -42,6 +42,13 @@ func GetVersionInfo() VersionInfo { return getVersionInfoWithValues(Version, Commit, BuildDate) } +// GetUserAgent returns a User-Agent string for HTTP clients +// Format: ToolHive/version (platform) go/version +func GetUserAgent() string { + info := GetVersionInfo() + return fmt.Sprintf("ToolHive/%s (%s) go/%s", info.Version, info.Platform, info.GoVersion) +} + // getVersionInfoWithValues returns version info with provided values (for testing) func getVersionInfoWithValues(version, commit, buildDate string) VersionInfo { ver := version From df0410d182226055525d59cb35ef1063193ceb55 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 7 Nov 2025 01:55:49 +0200 Subject: [PATCH 16/20] Extract environment variables from runtime arguments in MCP registry converter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support extracting environment variables from both sources in the MCP Registry API format: 1. The dedicated environmentVariables field (preferred way) 2. The -e/--env flags in runtimeArguments (Docker CLI pattern) Many servers in the MCP registry (like github-mcp-server) use the Docker CLI pattern of specifying environment variables as runtime arguments with -e flags, rather than using the dedicated environmentVariables field. This is a valid approach according to the MCP registry schema. Changes: - Add extractEnvironmentVariables() to handle both sources - Add extractEnvFromRuntimeArgs() to parse -e/--env flags - Add parseEnvVarFromValue() to extract variable metadata - Parse variable references like {token} and extract metadata (isSecret, isRequired, etc.) - Handle static values and complex values with multiple equals signs - Add comprehensive test suite with 15+ test cases covering all scenarios - Add integration test with realistic GitHub MCP server data This ensures ToolHive can properly consume all servers from the official MCP registry, regardless of which format they use for environment variables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../converters/envvar_extraction_test.go | 471 ++++++++++++++++++ .../converters/upstream_to_toolhive.go | 91 +++- 2 files changed, 560 insertions(+), 2 deletions(-) create mode 100644 pkg/registry/converters/envvar_extraction_test.go diff --git a/pkg/registry/converters/envvar_extraction_test.go b/pkg/registry/converters/envvar_extraction_test.go new file mode 100644 index 000000000..43486bfad --- /dev/null +++ b/pkg/registry/converters/envvar_extraction_test.go @@ -0,0 +1,471 @@ +package converters + +import ( + "testing" + + "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/registry/types" +) + +// Test extracting environment variables from runtime arguments (-e flags) +func TestExtractEnvFromRuntimeArgs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []model.Argument + expected []*types.EnvVar + }{ + { + name: "single -e flag with variable reference", + args: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "GITHUB_PERSONAL_ACCESS_TOKEN={token}", + Description: "Set an environment variable in the runtime", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "token": { + IsRequired: true, + IsSecret: true, + Format: "string", + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + }, + expected: []*types.EnvVar{ + { + Name: "GITHUB_PERSONAL_ACCESS_TOKEN", + Description: "Set an environment variable in the runtime", + Required: true, + Secret: true, + }, + }, + }, + { + name: "multiple -e flags", + args: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "API_KEY={key}", + Description: "API key", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "key": { + IsRequired: true, + IsSecret: true, + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "DEBUG=true", + Description: "Enable debug mode", + IsRequired: false, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + }, + expected: []*types.EnvVar{ + { + Name: "API_KEY", + Description: "API key", + Required: true, + Secret: true, + }, + { + Name: "DEBUG", + Description: "Enable debug mode", + Required: false, + Default: "true", + }, + }, + }, + { + name: "--env flag variant", + args: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "TOKEN={token}", + Description: "Auth token", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "token": { + IsRequired: true, + IsSecret: true, + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "--env", + }, + }, + expected: []*types.EnvVar{ + { + Name: "TOKEN", + Description: "Auth token", + Required: true, + Secret: true, + }, + }, + }, + { + name: "mixed with non-env arguments", + args: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "run", + }, + }, + Type: model.ArgumentTypePositional, + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "true", + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-i", + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "KEY=value", + Description: "Some key", + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "true", + }, + }, + Type: model.ArgumentTypeNamed, + Name: "--rm", + }, + }, + expected: []*types.EnvVar{ + { + Name: "KEY", + Description: "Some key", + Default: "value", + }, + }, + }, + { + name: "no environment arguments", + args: []model.Argument{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := extractEnvFromRuntimeArgs(tt.args) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test parsing environment variable values +func TestParseEnvVarFromValue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + description string + variables map[string]model.Input + expected *types.EnvVar + }{ + { + name: "static value", + value: "DEBUG=true", + description: "Enable debug", + variables: nil, + expected: &types.EnvVar{ + Name: "DEBUG", + Description: "Enable debug", + Default: "true", + }, + }, + { + name: "variable reference with metadata", + value: "API_KEY={key}", + description: "API key", + variables: map[string]model.Input{ + "key": { + IsRequired: true, + IsSecret: true, + Default: "default-key", + }, + }, + expected: &types.EnvVar{ + Name: "API_KEY", + Description: "API key", + Required: true, + Secret: true, + Default: "default-key", + }, + }, + { + name: "variable reference without metadata", + value: "TOKEN={token}", + description: "Auth token", + variables: map[string]model.Input{}, + expected: &types.EnvVar{ + Name: "TOKEN", + Description: "Auth token", + }, + }, + { + name: "empty value", + value: "", + description: "Something", + variables: nil, + expected: nil, + }, + { + name: "no equals sign", + value: "INVALID", + description: "Invalid", + variables: nil, + expected: nil, + }, + { + name: "complex value with equals", + value: "CONNECTION_STRING=host=localhost;port=5432", + description: "Database connection", + variables: nil, + expected: &types.EnvVar{ + Name: "CONNECTION_STRING", + Description: "Database connection", + Default: "host=localhost;port=5432", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := parseEnvVarFromValue(tt.value, tt.description, tt.variables) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Test extracting environment variables from both sources +func TestExtractEnvironmentVariables(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + pkg model.Package + expected []*types.EnvVar + }{ + { + name: "from environmentVariables field only", + pkg: model.Package{ + EnvironmentVariables: []model.KeyValueInput{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "API key", + IsRequired: true, + IsSecret: true, + }, + }, + Name: "API_KEY", + }, + }, + }, + expected: []*types.EnvVar{ + { + Name: "API_KEY", + Description: "API key", + Required: true, + Secret: true, + }, + }, + }, + { + name: "from runtimeArguments only", + pkg: model.Package{ + RuntimeArguments: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "TOKEN={token}", + Description: "Auth token", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "token": { + IsRequired: true, + IsSecret: true, + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + }, + }, + expected: []*types.EnvVar{ + { + Name: "TOKEN", + Description: "Auth token", + Required: true, + Secret: true, + }, + }, + }, + { + name: "from both sources combined", + pkg: model.Package{ + EnvironmentVariables: []model.KeyValueInput{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Description: "Variable 1", + IsRequired: true, + }, + }, + Name: "VAR1", + }, + }, + RuntimeArguments: []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "VAR2={var2}", + Description: "Variable 2", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "var2": { + IsRequired: true, + IsSecret: true, + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + }, + }, + expected: []*types.EnvVar{ + { + Name: "VAR1", + Description: "Variable 1", + Required: true, + }, + { + Name: "VAR2", + Description: "Variable 2", + Required: true, + Secret: true, + }, + }, + }, + { + name: "empty package", + pkg: model.Package{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := extractEnvironmentVariables(tt.pkg) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Integration test with realistic GitHub MCP server data +func TestServerJSONToImageMetadata_GitHubServerEnvVars(t *testing.T) { + t.Parallel() + + // Simulate the GitHub MCP server structure with -e flags + serverJSON := createTestServerJSON() + serverJSON.Name = "io.github.github/github-mcp-server" + serverJSON.Packages[0].RuntimeArguments = []model.Argument{ + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "run", + Description: "The runtime command to execute", + IsRequired: true, + }, + }, + Type: model.ArgumentTypePositional, + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "true", + Description: "Run container in interactive mode", + IsRequired: true, + Format: "boolean", + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-i", + }, + { + InputWithVariables: model.InputWithVariables{ + Input: model.Input{ + Value: "GITHUB_PERSONAL_ACCESS_TOKEN={token}", + Description: "Set an environment variable in the runtime", + IsRequired: true, + }, + Variables: map[string]model.Input{ + "token": { + IsRequired: true, + IsSecret: true, + Format: "string", + }, + }, + }, + Type: model.ArgumentTypeNamed, + Name: "-e", + }, + } + + result, err := ServerJSONToImageMetadata(serverJSON) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify environment variable was extracted + require.Len(t, result.EnvVars, 1) + assert.Equal(t, "GITHUB_PERSONAL_ACCESS_TOKEN", result.EnvVars[0].Name) + assert.Equal(t, "Set an environment variable in the runtime", result.EnvVars[0].Description) + assert.True(t, result.EnvVars[0].Required) + assert.True(t, result.EnvVars[0].Secret) +} diff --git a/pkg/registry/converters/upstream_to_toolhive.go b/pkg/registry/converters/upstream_to_toolhive.go index 676fcb289..444b40286 100644 --- a/pkg/registry/converters/upstream_to_toolhive.go +++ b/pkg/registry/converters/upstream_to_toolhive.go @@ -47,8 +47,8 @@ func ServerJSONToImageMetadata(serverJSON *upstream.ServerJSON) (*types.ImageMet imageMetadata.RepositoryURL = serverJSON.Repository.URL } - // Convert environment variables - imageMetadata.EnvVars = convertEnvironmentVariables(pkg.EnvironmentVariables) + // Convert environment variables from both sources + imageMetadata.EnvVars = extractEnvironmentVariables(pkg) // Extract target port from transport URL if present imageMetadata.TargetPort = extractTargetPort(pkg.Transport.URL, serverJSON.Name) @@ -91,6 +91,21 @@ func extractSingleOCIPackage(serverJSON *upstream.ServerJSON) (model.Package, er return ociPackages[0], nil } +// extractEnvironmentVariables extracts environment variables from both sources: +// 1. The direct environmentVariables field (preferred) +// 2. The -e/--env flags in runtimeArguments (Docker CLI pattern) +func extractEnvironmentVariables(pkg model.Package) []*types.EnvVar { + var envVars []*types.EnvVar + + // First, extract from the dedicated environmentVariables field + envVars = append(envVars, convertEnvironmentVariables(pkg.EnvironmentVariables)...) + + // Second, extract from -e/--env flags in runtimeArguments + envVars = append(envVars, extractEnvFromRuntimeArgs(pkg.RuntimeArguments)...) + + return envVars +} + // convertEnvironmentVariables converts model.KeyValueInput to types.EnvVar func convertEnvironmentVariables(envVars []model.KeyValueInput) []*types.EnvVar { if len(envVars) == 0 { @@ -110,6 +125,78 @@ func convertEnvironmentVariables(envVars []model.KeyValueInput) []*types.EnvVar return result } +// extractEnvFromRuntimeArgs extracts environment variables from -e/--env flags in runtime arguments +// This handles the Docker CLI pattern where env vars are specified as: -e KEY=value or --env KEY=value +func extractEnvFromRuntimeArgs(args []model.Argument) []*types.EnvVar { + var result []*types.EnvVar + + for _, arg := range args { + // Skip if not a named argument with -e or --env + if arg.Type != model.ArgumentTypeNamed { + continue + } + if arg.Name != "-e" && arg.Name != "--env" { + continue + } + + // Parse the environment variable from the value + // Format: KEY=value or KEY={variableName} + envVar := parseEnvVarFromValue(arg.Value, arg.Description, arg.Variables) + if envVar != nil { + envVar.Required = arg.IsRequired + result = append(result, envVar) + } + } + + return result +} + +// parseEnvVarFromValue parses an environment variable definition from a value string +// Handles formats like: KEY=value, KEY={varName}, etc. +func parseEnvVarFromValue(value, description string, variables map[string]model.Input) *types.EnvVar { + if value == "" { + return nil + } + + // Find the = separator + eqIdx := -1 + for i, ch := range value { + if ch == '=' { + eqIdx = i + break + } + } + + if eqIdx == -1 { + return nil // No = found, invalid format + } + + name := value[:eqIdx] + valuePart := value[eqIdx+1:] + + envVar := &types.EnvVar{ + Name: name, + Description: description, + } + + // Check if the value contains a variable reference like {token} + if len(valuePart) > 2 && valuePart[0] == '{' && valuePart[len(valuePart)-1] == '}' { + varName := valuePart[1 : len(valuePart)-1] + if varDef, ok := variables[varName]; ok { + envVar.Required = varDef.IsRequired + envVar.Secret = varDef.IsSecret + if varDef.Default != "" { + envVar.Default = varDef.Default + } + } + } else { + // Static value provided + envVar.Default = valuePart + } + + return envVar +} + // extractTargetPort extracts the port number from a transport URL func extractTargetPort(transportURL, serverName string) int { if transportURL == "" { From 7affa01e325fb5ac44799ac3766112e30cf62469 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 7 Nov 2025 15:53:09 +0200 Subject: [PATCH 17/20] Implement caching Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/registry.go | 25 +- pkg/config/registry.go | 1 + pkg/registry/factory.go | 4 +- pkg/registry/provider_cached.go | 413 ++++++++++++++++++++++++++++++++ 4 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 pkg/registry/provider_cached.go diff --git a/cmd/thv/app/registry.go b/cmd/thv/app/registry.go index 48b3a1b73..159080921 100644 --- a/cmd/thv/app/registry.go +++ b/cmd/thv/app/registry.go @@ -37,7 +37,8 @@ var registryInfoCmd = &cobra.Command{ } var ( - registryFormat string + registryFormat string + refreshRegistry bool ) func init() { @@ -50,7 +51,9 @@ func init() { // Add flags for list and info commands registryListCmd.Flags().StringVar(®istryFormat, "format", FormatText, "Output format (json or text)") + registryListCmd.Flags().BoolVar(&refreshRegistry, "refresh", false, "Force refresh registry cache") registryInfoCmd.Flags().StringVar(®istryFormat, "format", FormatText, "Output format (json or text)") + registryInfoCmd.Flags().BoolVar(&refreshRegistry, "refresh", false, "Force refresh registry cache") } func registryListCmdFunc(_ *cobra.Command, _ []string) error { @@ -59,6 +62,16 @@ func registryListCmdFunc(_ *cobra.Command, _ []string) error { if err != nil { return fmt.Errorf("failed to get registry provider: %v", err) } + + // Force refresh if requested + if refreshRegistry { + if cached, ok := provider.(*registry.CachedAPIRegistryProvider); ok { + if err := cached.ForceRefresh(); err != nil { + return fmt.Errorf("failed to refresh registry: %v", err) + } + } + } + servers, err := provider.ListServers() if err != nil { return fmt.Errorf("failed to list servers: %v", err) @@ -84,6 +97,16 @@ func registryInfoCmdFunc(_ *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("failed to get registry provider: %v", err) } + + // Force refresh if requested + if refreshRegistry { + if cached, ok := provider.(*registry.CachedAPIRegistryProvider); ok { + if err := cached.ForceRefresh(); err != nil { + return fmt.Errorf("failed to refresh registry: %v", err) + } + } + } + server, err := provider.GetServer(serverName) if err != nil { return fmt.Errorf("failed to get server information: %v", err) diff --git a/pkg/config/registry.go b/pkg/config/registry.go index 3fc14eb29..d1efbe362 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -2,6 +2,7 @@ package config import ( "fmt" + neturl "net/url" "path/filepath" "strings" diff --git a/pkg/registry/factory.go b/pkg/registry/factory.go index f1f77a48c..79d5ebda4 100644 --- a/pkg/registry/factory.go +++ b/pkg/registry/factory.go @@ -26,7 +26,9 @@ func NewRegistryProvider(cfg *config.Config) Provider { // 4. Default - embedded registry data if cfg != nil && len(cfg.RegistryApiUrl) > 0 { - provider, err := NewAPIRegistryProvider(cfg.RegistryApiUrl, cfg.AllowPrivateRegistryIp) + // Use cached provider with persistent cache enabled by default + // This provides 1-hour TTL and works for both CLI and API server + provider, err := NewCachedAPIRegistryProvider(cfg.RegistryApiUrl, cfg.AllowPrivateRegistryIp, true) if err != nil { // Log error but fall back to default provider // This prevents application from failing if API is temporarily unavailable diff --git a/pkg/registry/provider_cached.go b/pkg/registry/provider_cached.go new file mode 100644 index 000000000..dab9e6c7a --- /dev/null +++ b/pkg/registry/provider_cached.go @@ -0,0 +1,413 @@ +package registry + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/adrg/xdg" + v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + + "github.com/stacklok/toolhive/pkg/registry/types" +) + +const ( + // Cache configuration (hardcoded to avoid config pollution) + defaultCacheTTL = 1 * time.Hour + maxCacheFileSize = 10 * 1024 * 1024 // 10MB per cache file + maxCacheAge = 7 * 24 * time.Hour // Delete caches older than 7 days + maxTotalCacheSize = 50 * 1024 * 1024 // 50MB total cache directory + persistentCacheSubdir = "cache" +) + +// CachedAPIRegistryProvider wraps APIRegistryProvider with caching support. +// Provides both in-memory and optional persistent file caching. +// Works for both CLI (with persistent cache) and API server (memory only). +type CachedAPIRegistryProvider struct { + *APIRegistryProvider + + // In-memory cache + cacheMu sync.RWMutex + cachedData *types.Registry + cacheTime time.Time + + // Cache configuration + cacheTTL time.Duration + usePersistent bool + cacheFile string +} + +// NewCachedAPIRegistryProvider creates a new cached API registry provider. +// If usePersistent is true, it will use a file cache in ~/.toolhive/cache/ +func NewCachedAPIRegistryProvider(apiURL string, allowPrivateIp bool, usePersistent bool) (*CachedAPIRegistryProvider, error) { + base, err := NewAPIRegistryProvider(apiURL, allowPrivateIp) + if err != nil { + return nil, err + } + + cached := &CachedAPIRegistryProvider{ + APIRegistryProvider: base, + cacheTTL: defaultCacheTTL, + usePersistent: usePersistent, + } + + // CRITICAL: Override the BaseProvider's GetRegistryFunc to use our cached version + // Without this, BaseProvider.ListServers() will call the uncached APIRegistryProvider.GetRegistry() + // which hits the API and does expensive conversion on every call + cached.GetRegistryFunc = cached.GetRegistry + + if usePersistent { + // Generate cache file path based on API URL hash + hash := sha256.Sum256([]byte(apiURL)) + cacheFile, err := xdg.CacheFile(fmt.Sprintf("toolhive/%s/registry-%x.json", persistentCacheSubdir, hash[:4])) + if err != nil { + return nil, fmt.Errorf("failed to get cache file path: %w", err) + } + cached.cacheFile = cacheFile + + // Clean up old caches + cached.cleanupOldCaches() + + // Try to load from disk + if err := cached.loadFromDisk(); err != nil { + // Not a fatal error, just means we'll fetch from API + _ = err + } + } + + return cached, nil +} + +// GetRegistry returns the registry data, using cache if valid. +// Falls back to stale cache if API is unavailable. +func (p *CachedAPIRegistryProvider) GetRegistry() (*types.Registry, error) { + p.cacheMu.RLock() + + // Check if cache is valid (not expired) + if p.cachedData != nil && time.Since(p.cacheTime) < p.cacheTTL { + defer p.cacheMu.RUnlock() + return p.cachedData, nil + } + p.cacheMu.RUnlock() + + // Cache expired or missing, fetch fresh data + return p.refreshCache() +} + +// refreshCache fetches fresh data from the API and updates the cache. +// If the API fetch fails, returns stale cache if available. +func (p *CachedAPIRegistryProvider) refreshCache() (*types.Registry, error) { + p.cacheMu.Lock() + defer p.cacheMu.Unlock() + + // Fetch from API + registry, err := p.APIRegistryProvider.GetRegistry() + if err != nil { + // If fetch fails and we have stale cache, return it + if p.cachedData != nil { + return p.cachedData, nil + } + return nil, err + } + + // Update in-memory cache + p.cachedData = registry + p.cacheTime = time.Now() + + // Persist to disk if enabled + if p.usePersistent { + if err := p.saveToDisk(registry); err != nil { + // Log error but don't fail - cache save is non-critical + _ = err + } + } + + return registry, nil +} + +// ForceRefresh forces a cache refresh, ignoring TTL. +func (p *CachedAPIRegistryProvider) ForceRefresh() error { + _, err := p.refreshCache() + return err +} + +// GetServer returns a specific server by name (overrides base to use cache). +func (p *CachedAPIRegistryProvider) GetServer(name string) (types.ServerMetadata, error) { + // For individual server lookups, we could query the API directly for freshness, + // or use the cached registry. Let's use cached registry for consistency. + registry, err := p.GetRegistry() + if err != nil { + return nil, err + } + + // Try to find in cached registry first + if server, ok := registry.Servers[name]; ok { + return server, nil + } + if server, ok := registry.RemoteServers[name]; ok { + return server, nil + } + + // Fall back to API lookup (might be a newly added server) + return p.APIRegistryProvider.GetServer(name) +} + +// SearchServers searches for servers, using cached data. +func (p *CachedAPIRegistryProvider) SearchServers(query string) ([]types.ServerMetadata, error) { + // Ensure cache is loaded first + _, err := p.GetRegistry() + if err != nil { + return nil, err + } + + // Use base provider's SearchServers which will use our GetRegistry + return p.BaseProvider.SearchServers(query) +} + +// ListServers returns all servers from cache. +func (p *CachedAPIRegistryProvider) ListServers() ([]types.ServerMetadata, error) { + // Ensure cache is loaded first + _, err := p.GetRegistry() + if err != nil { + return nil, err + } + + // Use base provider's ListServers which will use our GetRegistry + return p.BaseProvider.ListServers() +} + +// loadFromDisk loads cached data from disk if available and valid. +func (p *CachedAPIRegistryProvider) loadFromDisk() error { + if p.cacheFile == "" { + return fmt.Errorf("no cache file configured") + } + + // Check if file exists + info, err := os.Stat(p.cacheFile) + if err != nil { + return err + } + + // Check cache age + if time.Since(info.ModTime()) > maxCacheAge { + // Cache too old, delete it + _ = os.Remove(p.cacheFile) + return fmt.Errorf("cache too old, deleted") + } + + // Check file size + if info.Size() > maxCacheFileSize { + // Cache file too large, delete it + _ = os.Remove(p.cacheFile) + return fmt.Errorf("cache file too large, deleted") + } + + // Read file + data, err := os.ReadFile(p.cacheFile) + if err != nil { + return err + } + + // Parse JSON + var registry types.Registry + if err := json.Unmarshal(data, ®istry); err != nil { + // Corrupted cache, delete it + _ = os.Remove(p.cacheFile) + return fmt.Errorf("corrupted cache, deleted: %w", err) + } + + // Load into memory + p.cacheMu.Lock() + p.cachedData = ®istry + p.cacheTime = info.ModTime() + p.cacheMu.Unlock() + + return nil +} + +// saveToDisk saves the current cache to disk. +func (p *CachedAPIRegistryProvider) saveToDisk(registry *types.Registry) error { + if p.cacheFile == "" { + return fmt.Errorf("no cache file configured") + } + + // Marshal to JSON + data, err := json.MarshalIndent(registry, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal cache: %w", err) + } + + // Check size before writing + if len(data) > maxCacheFileSize { + return fmt.Errorf("cache data too large: %d bytes", len(data)) + } + + // Write atomically using temp file + rename + tmpFile := p.cacheFile + ".tmp" + if err := os.WriteFile(tmpFile, data, 0o600); err != nil { + return fmt.Errorf("failed to write cache: %w", err) + } + + if err := os.Rename(tmpFile, p.cacheFile); err != nil { + _ = os.Remove(tmpFile) + return fmt.Errorf("failed to rename cache: %w", err) + } + + return nil +} + +// cleanupOldCaches removes old cache files to prevent unbounded growth. +// +//nolint:gocyclo // Cache cleanup logic naturally has complexity due to multiple passes +func (p *CachedAPIRegistryProvider) cleanupOldCaches() { + if p.cacheFile == "" { + return + } + + cacheDir := filepath.Dir(p.cacheFile) + + // Get all cache files + entries, err := os.ReadDir(cacheDir) + if err != nil { + return + } + + now := time.Now() + var totalSize int64 + + // First pass: delete old files and calculate total size + for _, entry := range entries { + if entry.IsDir() { + continue + } + + path := filepath.Join(cacheDir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + // Delete files older than maxCacheAge + if now.Sub(info.ModTime()) > maxCacheAge { + _ = os.Remove(path) + continue + } + + totalSize += info.Size() + } + + // If total size exceeds limit, delete oldest files + if totalSize > maxTotalCacheSize { + // Re-read directory after deletions + entries, err := os.ReadDir(cacheDir) + if err != nil { + return + } + + // Sort by modification time (oldest first) + type fileInfo struct { + path string + modTime time.Time + size int64 + } + + var files []fileInfo + for _, entry := range entries { + if entry.IsDir() { + continue + } + + path := filepath.Join(cacheDir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + files = append(files, fileInfo{ + path: path, + modTime: info.ModTime(), + size: info.Size(), + }) + } + + // Sort by modification time + for i := 0; i < len(files); i++ { + for j := i + 1; j < len(files); j++ { + if files[i].modTime.After(files[j].modTime) { + files[i], files[j] = files[j], files[i] + } + } + } + + // Delete oldest files until under limit + for _, f := range files { + if totalSize <= maxTotalCacheSize { + break + } + + if err := os.Remove(f.path); err == nil { + totalSize -= f.size + } + } + } +} + +// Ensure CachedAPIRegistryProvider implements Provider interface +var _ Provider = (*CachedAPIRegistryProvider)(nil) + +// Override methods that query individual servers to ensure they use cache + +// GetImageServer returns a specific container server by name (uses cache). +func (p *CachedAPIRegistryProvider) GetImageServer(name string) (*types.ImageMetadata, error) { + server, err := p.GetServer(name) + if err != nil { + return nil, err + } + + if img, ok := server.(*types.ImageMetadata); ok { + return img, nil + } + + return nil, fmt.Errorf("server %s is not a container server", name) +} + +// GetRemoteServer returns a specific remote server by name (uses cache). +func (p *CachedAPIRegistryProvider) GetRemoteServer(name string) (*types.RemoteServerMetadata, error) { + server, err := p.GetServer(name) + if err != nil { + return nil, err + } + + if remote, ok := server.(*types.RemoteServerMetadata); ok { + return remote, nil + } + + return nil, fmt.Errorf("server %s is not a remote server", name) +} + +// ConvertServerJSON wraps ConvertServerJSON for cached provider +func (*CachedAPIRegistryProvider) ConvertServerJSON(serverJSON *v0.ServerJSON) (types.ServerMetadata, error) { + return ConvertServerJSON(serverJSON) +} + +// ConvertServersToMetadataWithCache wraps ConvertServersToMetadata for cached provider +func (*CachedAPIRegistryProvider) ConvertServersToMetadataWithCache(servers []*v0.ServerJSON) ([]types.ServerMetadata, error) { + return ConvertServersToMetadata(servers) +} + +// GetServerWithContext returns a specific server by name with context support +func (p *CachedAPIRegistryProvider) GetServerWithContext(ctx context.Context, name string) (types.ServerMetadata, error) { + // Check if context is already cancelled + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + return p.GetServer(name) +} From 1917edfa356e5b277b251f6dc75aade0c2c20140 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 7 Nov 2025 16:10:44 +0200 Subject: [PATCH 18/20] Update CLI documentation for registry commands Signed-off-by: Radoslav Dimitrov --- docs/cli/thv_registry_info.md | 1 + docs/cli/thv_registry_list.md | 1 + docs/server/docs.go | 6 +----- docs/server/swagger.json | 6 +----- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/cli/thv_registry_info.md b/docs/cli/thv_registry_info.md index cb50bd2d2..45d2f0ecb 100644 --- a/docs/cli/thv_registry_info.md +++ b/docs/cli/thv_registry_info.md @@ -26,6 +26,7 @@ thv registry info [server] [flags] ``` --format string Output format (json or text) (default "text") -h, --help help for info + --refresh Force refresh registry cache ``` ### Options inherited from parent commands diff --git a/docs/cli/thv_registry_list.md b/docs/cli/thv_registry_list.md index a2f9dfe24..2feff6cec 100644 --- a/docs/cli/thv_registry_list.md +++ b/docs/cli/thv_registry_list.md @@ -26,6 +26,7 @@ thv registry list [flags] ``` --format string Output format (json or text) (default "text") -h, --help help for list + --refresh Force refresh registry cache ``` ### Options inherited from parent commands diff --git a/docs/server/docs.go b/docs/server/docs.go index 2c6491042..d5467021d 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -6,11 +6,7 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, -<<<<<<< HEAD - "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"}}}, -======= - "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, ->>>>>>> e59f7fb6 (Fix CodeQL warning and regenerate swagger docs) + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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 426b1ee9d..fad9dc1be 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -1,9 +1,5 @@ { -<<<<<<< HEAD - "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"}}}, -======= - "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, ->>>>>>> e59f7fb6 (Fix CodeQL warning and regenerate swagger docs) + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"]}}}, From b536473cbed7ee691042ba13efc48869df21cf5c Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Mon, 10 Nov 2025 15:35:07 +0200 Subject: [PATCH 19/20] Regenerate the swagger files Signed-off-by: Radoslav Dimitrov --- cmd/thv-proxyrunner/app/run.go | 2 +- docs/server/docs.go | 2 +- docs/server/swagger.json | 2 +- docs/server/swagger.yaml | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/thv-proxyrunner/app/run.go b/cmd/thv-proxyrunner/app/run.go index ebc4076bd..55fdf9534 100644 --- a/cmd/thv-proxyrunner/app/run.go +++ b/cmd/thv-proxyrunner/app/run.go @@ -161,7 +161,7 @@ func runWithFileBasedConfig( rt runtime.Runtime, debugMode bool, envVarValidator runner.EnvVarValidator, - imageMetadata *registry.ImageMetadata, + imageMetadata *regtypes.ImageMetadata, ) error { // Use the file config directly with minimal essential overrides config.Image = mcpServerImage diff --git a/docs/server/docs.go b/docs/server/docs.go index d5467021d..4226bc1ab 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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707).","type":"string"},"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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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"},"resource":{"description":"OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"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/types.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 fad9dc1be..d35aa3867 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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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/types.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"}}}, + "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"},"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/types.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/types.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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707).","type":"string"},"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.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"},"types.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/types.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/types.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions within this group","type":"object"}},"type":"object"},"types.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"},"types.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/types.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.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"},"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.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"},"resource":{"description":"Resource is the OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"types.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/types.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"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.Registry":{"description":"Full registry data","properties":{"groups":{"description":"Groups is a slice of group definitions containing related MCP servers","items":{"$ref":"#/components/schemas/types.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/types.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/types.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"},"types.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/types.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/types.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/types.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/types.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"},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"types.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeAPI","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL or API URL","type":"boolean"},"api_url":{"description":"MCP Registry API URL","type":"string"},"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/types.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/types.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","RegistryTypeAPI","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/types.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/types.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/types.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/types.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"},"resource":{"description":"OAuth 2.0 resource indicator (RFC 8707)","type":"string"},"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/types.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 9f6ef9eb6..faa46d944 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -366,6 +366,9 @@ components: type: string description: OAuth parameters for server-specific customization type: object + resource: + description: Resource is the OAuth 2.0 resource indicator (RFC 8707). + type: string scopes: items: type: string @@ -848,6 +851,9 @@ components: OAuthParams contains additional OAuth parameters to include in the authorization request These are server-specific parameters like "prompt", "response_mode", etc. type: object + resource: + description: Resource is the OAuth 2.0 resource indicator (RFC 8707) + type: string scopes: description: |- Scopes are the OAuth scopes to request @@ -1466,6 +1472,9 @@ components: type: string description: Additional OAuth parameters for server-specific customization type: object + resource: + description: OAuth 2.0 resource indicator (RFC 8707) + type: string scopes: description: OAuth scopes to request items: From f37a2fadae6efa7b4c2d3235d44b886f65db1c19 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 11 Nov 2025 00:54:11 +0200 Subject: [PATCH 20/20] Consolidate set-registry-api into set-registry Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/config.go | 75 ++++++++----------------- docs/cli/thv_config.md | 1 - docs/cli/thv_config_set-registry-api.md | 48 ---------------- docs/cli/thv_config_set-registry.md | 12 ++-- pkg/config/registry.go | 7 ++- 5 files changed, 38 insertions(+), 105 deletions(-) delete mode 100644 docs/cli/thv_config_set-registry-api.md diff --git a/cmd/thv/app/config.go b/cmd/thv/app/config.go index 66e58d4f8..1cea478af 100644 --- a/cmd/thv/app/config.go +++ b/cmd/thv/app/config.go @@ -46,33 +46,21 @@ var unsetCACertCmd = &cobra.Command{ var setRegistryCmd = &cobra.Command{ Use: "set-registry ", Short: "Set the MCP server registry", - Long: `Set the MCP server registry to either a remote URL or local file path. -The command automatically detects whether the input is a URL or file path. + Long: `Set the MCP server registry to a remote URL, local file path, or API endpoint. +The command automatically detects the registry type: + - URLs ending with .json are treated as static registry files + - Other URLs are treated as MCP Registry API endpoints (v0.1 spec) + - Local paths are treated as local registry files Examples: - thv config set-registry https://example.com/registry.json # Remote URL + thv config set-registry https://example.com/registry.json # Static remote file + thv config set-registry https://registry.example.com # API endpoint thv config set-registry /path/to/local-registry.json # Local file path thv config set-registry file:///path/to/local-registry.json # Explicit file URL`, Args: cobra.ExactArgs(1), RunE: setRegistryCmdFunc, } -var setRegistryAPICmd = &cobra.Command{ - Use: "set-registry-api ", - Short: "Set the MCP Registry API endpoint", - Long: `Set the MCP Registry API endpoint that implements the MCP Registry API v0.1 specification. -This enables on-demand querying of servers from a live registry API. - -The API endpoint must implement the official MCP Registry API specification from -https://registry.modelcontextprotocol.io/docs - -Examples: - thv config set-registry-api https://registry.example.com # API endpoint - thv config set-registry-api https://api.example.com --allow-private-ip # With private IP support`, - Args: cobra.ExactArgs(1), - RunE: setRegistryAPICmdFunc, -} - var getRegistryCmd = &cobra.Command{ Use: "get-registry", Short: "Get the currently configured registry", @@ -112,15 +100,7 @@ func init() { "allow-private-ip", "p", false, - "Allow setting the registry URL, even if it references a private IP address", - ) - configCmd.AddCommand(setRegistryAPICmd) - setRegistryAPICmd.Flags().BoolVarP( - &allowPrivateRegistryIp, - "allow-private-ip", - "p", - false, - "Allow setting the registry API URL, even if it references a private IP address", + "Allow setting the registry URL or API endpoint, even if it references a private IP address", ) configCmd.AddCommand(getRegistryCmd) configCmd.AddCommand(unsetRegistryCmd) @@ -193,16 +173,30 @@ func setRegistryCmdFunc(_ *cobra.Command, args []string) error { } // Reset the cached provider so it re-initializes with the new config registry.ResetDefaultProvider() - fmt.Printf("Successfully set registry URL: %s\n", cleanPath) + fmt.Printf("Successfully set static registry file: %s\n", cleanPath) if allowPrivateRegistryIp { fmt.Print("Successfully enabled use of private IP addresses for the remote registry\n") fmt.Print("Caution: allowing registry URLs containing private IP addresses may decrease your security.\n" + - "Make sure you trust any remote registries you configure with ToolHive.") + "Make sure you trust any remote registries you configure with ToolHive.\n") } else { fmt.Printf("Use of private IP addresses for the remote registry has been disabled" + " as it's not needed for the provided registry.\n") } return nil + case config.RegistryTypeAPI: + err := provider.SetRegistryAPI(cleanPath, allowPrivateRegistryIp) + if err != nil { + return err + } + // Reset the cached provider so it re-initializes with the new config + registry.ResetDefaultProvider() + fmt.Printf("Successfully set registry API endpoint: %s\n", cleanPath) + if allowPrivateRegistryIp { + fmt.Print("Successfully enabled use of private IP addresses for the registry API\n") + fmt.Print("Caution: allowing registry API URLs containing private IP addresses may decrease your security.\n" + + "Make sure you trust any registry APIs you configure with ToolHive.\n") + } + return nil case config.RegistryTypeFile: err := provider.SetRegistryFile(cleanPath) if err != nil { @@ -217,27 +211,6 @@ func setRegistryCmdFunc(_ *cobra.Command, args []string) error { } } -func setRegistryAPICmdFunc(_ *cobra.Command, args []string) error { - apiURL := args[0] - provider := config.NewDefaultProvider() - - err := provider.SetRegistryAPI(apiURL, allowPrivateRegistryIp) - if err != nil { - return err - } - - // Reset the cached provider so it re-initializes with the new config - registry.ResetDefaultProvider() - - fmt.Printf("Successfully set registry API endpoint: %s\n", apiURL) - if allowPrivateRegistryIp { - fmt.Print("Successfully enabled use of private IP addresses for the registry API\n") - fmt.Print("Caution: allowing registry API URLs containing private IP addresses may decrease your security.\n" + - "Make sure you trust any registry APIs you configure with ToolHive.\n") - } - return nil -} - func getRegistryCmdFunc(_ *cobra.Command, _ []string) error { provider := config.NewDefaultProvider() url, localPath, _, registryType := provider.GetRegistryConfig() diff --git a/docs/cli/thv_config.md b/docs/cli/thv_config.md index 5f256b52c..8a503af4a 100644 --- a/docs/cli/thv_config.md +++ b/docs/cli/thv_config.md @@ -37,7 +37,6 @@ The config command provides subcommands to manage application configuration sett * [thv config otel](thv_config_otel.md) - Manage OpenTelemetry configuration * [thv config set-ca-cert](thv_config_set-ca-cert.md) - Set the default CA certificate for container builds * [thv config set-registry](thv_config_set-registry.md) - Set the MCP server registry -* [thv config set-registry-api](thv_config_set-registry-api.md) - Set the MCP Registry API endpoint * [thv config unset-ca-cert](thv_config_unset-ca-cert.md) - Remove the configured CA certificate * [thv config unset-registry](thv_config_unset-registry.md) - Remove the configured registry * [thv config usage-metrics](thv_config_usage-metrics.md) - Enable or disable anonymous usage metrics diff --git a/docs/cli/thv_config_set-registry-api.md b/docs/cli/thv_config_set-registry-api.md deleted file mode 100644 index f35508314..000000000 --- a/docs/cli/thv_config_set-registry-api.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: thv config set-registry-api -hide_title: true -description: Reference for ToolHive CLI command `thv config set-registry-api` -last_update: - author: autogenerated -slug: thv_config_set-registry-api -mdx: - format: md ---- - -## thv config set-registry-api - -Set the MCP Registry API endpoint - -### Synopsis - -Set the MCP Registry API endpoint that implements the MCP Registry API v0.1 specification. -This enables on-demand querying of servers from a live registry API. - -The API endpoint must implement the official MCP Registry API specification from -https://registry.modelcontextprotocol.io/docs - -Examples: - thv config set-registry-api https://registry.example.com # API endpoint - thv config set-registry-api https://api.example.com --allow-private-ip # With private IP support - -``` -thv config set-registry-api [flags] -``` - -### Options - -``` - -p, --allow-private-ip Allow setting the registry API URL, even if it references a private IP address - -h, --help help for set-registry-api -``` - -### Options inherited from parent commands - -``` - --debug Enable debug mode -``` - -### SEE ALSO - -* [thv config](thv_config.md) - Manage application configuration - diff --git a/docs/cli/thv_config_set-registry.md b/docs/cli/thv_config_set-registry.md index 376e03b5b..0a6725bef 100644 --- a/docs/cli/thv_config_set-registry.md +++ b/docs/cli/thv_config_set-registry.md @@ -15,11 +15,15 @@ Set the MCP server registry ### Synopsis -Set the MCP server registry to either a remote URL or local file path. -The command automatically detects whether the input is a URL or file path. +Set the MCP server registry to a remote URL, local file path, or API endpoint. +The command automatically detects the registry type: + - URLs ending with .json are treated as static registry files + - Other URLs are treated as MCP Registry API endpoints (v0.1 spec) + - Local paths are treated as local registry files Examples: - thv config set-registry https://example.com/registry.json # Remote URL + thv config set-registry https://example.com/registry.json # Static remote file + thv config set-registry https://registry.example.com # API endpoint thv config set-registry /path/to/local-registry.json # Local file path thv config set-registry file:///path/to/local-registry.json # Explicit file URL @@ -30,7 +34,7 @@ thv config set-registry [flags] ### Options ``` - -p, --allow-private-ip Allow setting the registry URL, even if it references a private IP address + -p, --allow-private-ip Allow setting the registry URL or API endpoint, even if it references a private IP address -h, --help help for set-registry ``` diff --git a/pkg/config/registry.go b/pkg/config/registry.go index d1efbe362..9a0e71c7d 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -27,7 +27,12 @@ func DetectRegistryType(input string) (registryType string, cleanPath string) { // Check for HTTP/HTTPS URLs if networking.IsURL(input) { - return RegistryTypeURL, input + // If URL ends with .json, treat as static registry file + // Otherwise, treat as MCP Registry API endpoint + if strings.HasSuffix(input, ".json") { + return RegistryTypeURL, input + } + return RegistryTypeAPI, input } // Default: treat as file path