diff --git a/.gitattributes b/.gitattributes index 7e864d188d..a47e0284c9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,12 +4,14 @@ # Do not edit below this line. Edits will be overwritten by gen_gitattributes.sh catalog/internal/server/openapi/api.go linguist-generated=true +catalog/internal/server/openapi/api_mcp_catalog_service.go linguist-generated=true catalog/internal/server/openapi/api_model_catalog_service.go linguist-generated=true catalog/internal/server/openapi/error.go linguist-generated=true catalog/internal/server/openapi/helpers.go linguist-generated=true catalog/internal/server/openapi/impl.go linguist-generated=true catalog/internal/server/openapi/logger.go linguist-generated=true catalog/internal/server/openapi/routers.go linguist-generated=true +catalog/pkg/openapi/api_mcp_catalog_service.go linguist-generated=true catalog/pkg/openapi/api_model_catalog_service.go linguist-generated=true catalog/pkg/openapi/client.go linguist-generated=true catalog/pkg/openapi/configuration.go linguist-generated=true @@ -20,6 +22,7 @@ catalog/pkg/openapi/model_base_resource_dates.go linguist-generated=true catalog/pkg/openapi/model_base_resource_list.go linguist-generated=true catalog/pkg/openapi/model_catalog_artifact.go linguist-generated=true catalog/pkg/openapi/model_catalog_artifact_list.go linguist-generated=true +catalog/pkg/openapi/model_catalog_asset_type.go linguist-generated=true catalog/pkg/openapi/model_catalog_label.go linguist-generated=true catalog/pkg/openapi/model_catalog_label_list.go linguist-generated=true catalog/pkg/openapi/model_catalog_metrics_artifact.go linguist-generated=true @@ -36,6 +39,17 @@ catalog/pkg/openapi/model_field_filter.go linguist-generated=true catalog/pkg/openapi/model_filter_option.go linguist-generated=true catalog/pkg/openapi/model_filter_option_range.go linguist-generated=true catalog/pkg/openapi/model_filter_options_list.go linguist-generated=true +catalog/pkg/openapi/model_mcp_artifact.go linguist-generated=true +catalog/pkg/openapi/model_mcp_deployment_mode.go linguist-generated=true +catalog/pkg/openapi/model_mcp_endpoints.go linguist-generated=true +catalog/pkg/openapi/model_mcp_security_indicator.go linguist-generated=true +catalog/pkg/openapi/model_mcp_server.go linguist-generated=true +catalog/pkg/openapi/model_mcp_server_list.go linguist-generated=true +catalog/pkg/openapi/model_mcp_server_status.go linguist-generated=true +catalog/pkg/openapi/model_mcp_tool.go linguist-generated=true +catalog/pkg/openapi/model_mcp_tool_access_type.go linguist-generated=true +catalog/pkg/openapi/model_mcp_tool_parameter.go linguist-generated=true +catalog/pkg/openapi/model_mcp_transport_type.go linguist-generated=true catalog/pkg/openapi/model_metadata_bool_value.go linguist-generated=true catalog/pkg/openapi/model_metadata_double_value.go linguist-generated=true catalog/pkg/openapi/model_metadata_int_value.go linguist-generated=true diff --git a/api/openapi/catalog.yaml b/api/openapi/catalog.yaml index 0420d638c4..622b5ad456 100644 --- a/api/openapi/catalog.yaml +++ b/api/openapi/catalog.yaml @@ -36,6 +36,79 @@ paths: $ref: "#/components/responses/InternalServerError" operationId: findLabels description: Gets a list of all `CatalogLabel` entities. + /api/model_catalog/v1alpha1/mcp_servers: + summary: Path used to get the list of MCP servers. + description: The REST endpoint/path used to list zero or more `McpServer` entities. + get: + summary: List All McpServers + tags: + - McpCatalogService + parameters: + - $ref: "#/components/parameters/name" + - name: q + description: Free-form keyword search used to filter MCP servers by name, description, or provider. + schema: + type: string + in: query + required: false + - $ref: "#/components/parameters/filterQuery" + - $ref: "#/components/parameters/namedQuery" + - $ref: "#/components/parameters/pageSize" + - $ref: "#/components/parameters/orderBy" + - $ref: "#/components/parameters/sortOrder" + - $ref: "#/components/parameters/nextPageToken" + responses: + "200": + $ref: "#/components/responses/McpServerListResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findMcpServers + description: Gets a list of all `McpServer` entities. + /api/model_catalog/v1alpha1/mcp_servers/filter_options: + description: Lists options for `filterQuery` when listing MCP servers. + get: + summary: Lists fields and available options that can be used in `filterQuery` on the list MCP servers endpoint. + tags: + - McpCatalogService + responses: + "200": + $ref: "#/components/responses/FilterOptionsResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findMcpServersFilterOptions + /api/model_catalog/v1alpha1/mcp_servers/{server_id}: + summary: Path used to get a single MCP server. + description: The REST endpoint/path used to get a single `McpServer` entity. + get: + summary: Get an McpServer + tags: + - McpCatalogService + responses: + "200": + $ref: "#/components/responses/McpServerResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: getMcpServer + description: Gets a single `McpServer` entity by ID. + parameters: + - name: server_id + description: A unique identifier for an `McpServer`. + schema: + type: string + in: path + required: true /api/model_catalog/v1alpha1/models: description: >- The REST endpoint/path used to list zero or more `CatalogModel` entities from all `CatalogSources`. @@ -143,6 +216,7 @@ paths: - ModelCatalogService parameters: - $ref: "#/components/parameters/name" + - $ref: "#/components/parameters/assetType" - $ref: "#/components/parameters/pageSize" - $ref: "#/components/parameters/orderBy" - $ref: "#/components/parameters/sortOrder" @@ -598,6 +672,16 @@ components: required: - items - $ref: "#/components/schemas/BaseResourceList" + CatalogAssetType: + description: |- + The type of assets managed by a catalog source. + - `models`: AI/ML models (default) + - `mcp_servers`: MCP (Model Context Protocol) servers + enum: + - models + - mcp_servers + type: string + default: models CatalogLabel: description: A catalog label. Labels are used to categorize catalog sources. Represented as a flexible map of string key-value pairs with a required 'name' field. type: object @@ -725,6 +809,12 @@ components: type: array items: type: string + assetType: + $ref: "#/components/schemas/CatalogAssetType" + description: |- + The type of assets this source manages. Defaults to "models" if not specified. + This field determines which loader processes the source and which API endpoints + return the assets. status: $ref: "#/components/schemas/CatalogSourceStatus" description: Current operational status of the catalog source. @@ -907,6 +997,266 @@ components: type: object additionalProperties: $ref: "#/components/schemas/FieldFilter" + McpArtifact: + description: An artifact for an MCP server (e.g., OCI image for local deployment). + type: object + required: + - uri + properties: + uri: + type: string + format: uri + description: URI where the artifact can be retrieved (e.g., OCI image URI). + example: oci://ghcr.io/dynatrace-oss/dynatrace-mcp-server:1.0.1 + createTimeSinceEpoch: + type: string + description: Timestamp when the artifact was created (milliseconds since epoch). + lastUpdateTimeSinceEpoch: + type: string + description: Timestamp when the artifact was last updated (milliseconds since epoch). + McpDeploymentMode: + description: |- + Deployment mode for the MCP server. + - `local`: Server deployed from OCI artifact in Kubernetes cluster + - `remote`: Server hosted externally, accessed via network endpoints + enum: + - local + - remote + type: string + default: local + McpEndpoints: + description: |- + Network endpoints for remote MCP servers. Contains URLs for different + transport protocols. At least one endpoint must be provided for remote servers. + type: object + properties: + http: + type: string + format: uri + description: HTTP REST endpoint URL. + example: https://api.mcpservers.org/github-mcp/v1 + sse: + type: string + format: uri + description: Server-Sent Events endpoint URL. + example: https://api.mcpservers.org/github-mcp/sse + McpSecurityIndicator: + description: Security indicators for an MCP server. + type: object + properties: + verifiedSource: + type: boolean + description: Whether the source has been verified. + secureEndpoint: + type: boolean + description: Whether the endpoint uses secure communication. + sast: + type: boolean + description: Whether static application security testing has been performed. + readOnlyTools: + type: boolean + description: Whether all tools are read-only. + McpServer: + description: An MCP (Model Context Protocol) server that provides tools for AI agents. + type: object + required: + - id + - name + properties: + id: + type: string + description: Unique identifier for the MCP server. + example: dynatrace-mcp + name: + type: string + description: Human-readable name of the MCP server. + example: Dynatrace MCP Server + source_id: + type: string + description: ID of the catalog source this server belongs to. + example: organization_mcp_servers + description: + type: string + description: Description of the MCP server and its capabilities. + logo: + type: string + format: uri + description: URL to the server's logo image. + license: + type: string + description: License identifier (SPDX format preferred) under which the MCP server is distributed. + example: Apache-2.0 + license_link: + type: string + format: uri + description: URL to the full license text or license file. + example: https://github.com/dynatrace-oss/dynatrace-mcp-server/blob/main/LICENSE + provider: + type: string + description: Organization or entity that provides the MCP server. + example: Dynatrace + version: + type: string + description: Version of the MCP server. + example: 1.0.1 + tags: + type: array + items: + type: string + description: Tags for categorizing and filtering MCP servers. + example: + - observability + - monitoring + - apm + tools: + type: array + items: + $ref: "#/components/schemas/McpTool" + description: List of tools exposed by this MCP server. + securityIndicators: + $ref: "#/components/schemas/McpSecurityIndicator" + documentationUrl: + type: string + format: uri + description: URL to the server's documentation. + repositoryUrl: + type: string + format: uri + description: URL to the server's source repository. + sourceCode: + type: string + description: Source code repository identifier (e.g., GitHub org/repo). + example: dynatrace-oss/dynatrace-mcp-server + lastUpdated: + type: string + format: date-time + description: When the server was last updated. + publishedDate: + type: string + format: date + description: When the server was first published. + artifacts: + type: array + items: + $ref: "#/components/schemas/McpArtifact" + description: Artifacts for this MCP server (e.g., OCI images for local deployments). + transports: + description: Supported transport types. For remote servers, this is derived from available endpoints. + type: array + items: + $ref: "#/components/schemas/McpTransportType" + readme: + type: string + description: Full README content in Markdown format. + deploymentMode: + $ref: "#/components/schemas/McpDeploymentMode" + endpoints: + $ref: "#/components/schemas/McpEndpoints" + customProperties: + description: |- + User provided custom properties which are not defined by its type. + Following the Model Registry pattern, tags are stored as MetadataStringValue + entries with empty string_value, and security indicators are stored as + MetadataBoolValue entries. + type: object + additionalProperties: + $ref: "#/components/schemas/MetadataValue" + McpServerList: + description: List of McpServer entities. + allOf: + - type: object + properties: + items: + description: Array of `McpServer` entities. + type: array + items: + $ref: "#/components/schemas/McpServer" + required: + - items + - $ref: "#/components/schemas/BaseResourceList" + McpTool: + description: A tool exposed by an MCP server. + type: object + required: + - name + - accessType + properties: + name: + type: string + description: Unique name of the tool within the MCP server. + example: execute_dql + description: + type: string + description: Description of what the tool does. + accessType: + $ref: "#/components/schemas/McpToolAccessType" + parameters: + type: array + items: + $ref: "#/components/schemas/McpToolParameter" + description: Parameters accepted by this tool. + revoked: + type: boolean + default: false + description: |- + Whether this tool has been revoked. Revoked tools should not be + invoked by AI agents. This allows for immediate disabling of + problematic tools without removing them from the registry. + revokedReason: + type: string + description: |- + Human-readable reason why the tool was revoked. This helps users + understand why the tool is unavailable and when it might be restored. + example: Security vulnerability CVE-2025-XXXX - pending patch + customProperties: + description: User provided custom properties for the tool. + type: object + additionalProperties: + $ref: "#/components/schemas/MetadataValue" + McpToolAccessType: + description: |- + Access type indicating what kind of operations the tool performs. + - `read_only`: Tool only reads data + - `read_write`: Tool can read and write data + - `execute`: Tool executes operations + enum: + - read_only + - read_write + - execute + type: string + McpToolParameter: + description: A parameter for an MCP tool. + type: object + required: + - name + - type + - required + properties: + name: + type: string + description: Name of the parameter. + example: query + type: + type: string + description: Data type of the parameter. + example: string + description: + type: string + description: Description of the parameter. + required: + type: boolean + description: Whether the parameter is required. + McpTransportType: + description: |- + Transport protocol used by the MCP server. + - `stdio`: Standard input/output streams + - `sse`: Server-Sent Events over HTTP + - `http`: Standard HTTP/REST + enum: + - stdio + - sse + - http + type: string MetadataBoolValue: description: A bool property value. type: object @@ -1203,6 +1553,18 @@ components: schema: $ref: "#/components/schemas/Error" description: Unexpected internal server error + McpServerListResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/McpServerList" + description: A response containing a list of McpServer entities. + McpServerResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/McpServer" + description: A response containing an `McpServer` entity. NotFound: content: application/json: @@ -1270,7 +1632,7 @@ components: artifactFilterQuery: value: "name='my-artifact' AND uri LIKE '%s3%'" name: filterQuery - description: | + description: |- A SQL-like query string to filter catalog artifacts. The query supports rich filtering capabilities with automatic type inference. **Supported Operators:** @@ -1331,7 +1693,7 @@ components: value: hardware_count.int_value summary: Order by custom integer property name: orderBy - description: | + description: |- Specifies the order by criteria for listing artifacts. **Standard Fields:** @@ -1373,7 +1735,7 @@ components: labelOrderBy: value: name name: orderBy - description: | + description: |- Specifies the key to order catalog labels by. You can provide any string key that may exist in the label maps. Labels that contain the specified key will be sorted by that key's value. Labels that don't contain the key will maintain @@ -1411,6 +1773,56 @@ components: $ref: "#/components/schemas/ArtifactTypeQueryParam" in: query required: false + assetType: + name: assetType + description: |- + Filter sources by asset type. + - `models`: Sources containing AI/ML models + - `mcp_servers`: Sources containing MCP (Model Context Protocol) servers + schema: + $ref: "#/components/schemas/CatalogAssetType" + in: query + required: false + namedQuery: + name: namedQuery + description: |- + Apply a pre-defined named query to filter the list of entities. Named queries + are configured in the catalog sources YAML file and provide reusable filter + presets for common filtering scenarios. + + **Configuration Example:** + ```yaml + namedQueries: + production_ready: + provider: + operator: "IN" + value: ["Dynatrace", "CNCF", "GitHub"] + verifiedSource: + operator: "=" + value: true + monitoring_tools: + deploymentMode: + operator: "=" + value: "local" + ``` + + **Usage:** + - Apply single named query: `?namedQuery=production_ready` + - Named queries work alongside other filters (filterQuery, name) + - If the named query doesn't exist, a 400 Bad Request is returned + + **Behavior:** + - Named query filters are applied in addition to any other filters + - Named queries can reference customProperties and standard fields + - Results must satisfy ALL conditions in the named query + schema: + type: string + in: query + required: false + examples: + namedQuery: + value: production_ready + summary: Apply production_ready named query id: name: id description: The ID of resource. diff --git a/api/openapi/src/catalog.yaml b/api/openapi/src/catalog.yaml index 0ca1746c3f..1ec09f91ae 100644 --- a/api/openapi/src/catalog.yaml +++ b/api/openapi/src/catalog.yaml @@ -143,6 +143,7 @@ paths: - ModelCatalogService parameters: - $ref: "#/components/parameters/name" + - $ref: "#/components/parameters/assetType" - $ref: "#/components/parameters/pageSize" - $ref: "#/components/parameters/orderBy" - $ref: "#/components/parameters/sortOrder" @@ -302,9 +303,7 @@ paths: - $ref: "#/components/parameters/nextPageToken" /api/model_catalog/v1alpha1/sources/preview: description: >- - The REST endpoint/path used to preview a catalog source configuration. - This endpoint accepts a catalog source definition and returns a list of - models with their inclusion/exclusion status based on the configured filters. + The REST endpoint/path used to preview a catalog source configuration. This endpoint accepts a catalog source definition and returns a list of models with their inclusion/exclusion status based on the configured filters. post: summary: Preview catalog source configuration description: |- @@ -452,6 +451,79 @@ paths: "500": $ref: "#/components/responses/InternalServerError" operationId: previewCatalogSource + /api/model_catalog/v1alpha1/mcp_servers: + summary: Path used to get the list of MCP servers. + description: The REST endpoint/path used to list zero or more `McpServer` entities. + get: + summary: List All McpServers + tags: + - McpCatalogService + parameters: + - $ref: "#/components/parameters/name" + - name: q + description: Free-form keyword search used to filter MCP servers by name, description, or provider. + schema: + type: string + in: query + required: false + - $ref: "#/components/parameters/filterQuery" + - $ref: "#/components/parameters/namedQuery" + - $ref: "#/components/parameters/pageSize" + - $ref: "#/components/parameters/orderBy" + - $ref: "#/components/parameters/sortOrder" + - $ref: "#/components/parameters/nextPageToken" + responses: + "200": + $ref: "#/components/responses/McpServerListResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findMcpServers + description: Gets a list of all `McpServer` entities. + /api/model_catalog/v1alpha1/mcp_servers/filter_options: + description: Lists options for `filterQuery` when listing MCP servers. + get: + summary: Lists fields and available options that can be used in `filterQuery` on the list MCP servers endpoint. + tags: + - McpCatalogService + responses: + "200": + $ref: "#/components/responses/FilterOptionsResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findMcpServersFilterOptions + /api/model_catalog/v1alpha1/mcp_servers/{server_id}: + summary: Path used to get a single MCP server. + description: The REST endpoint/path used to get a single `McpServer` entity. + get: + summary: Get an McpServer + tags: + - McpCatalogService + responses: + "200": + $ref: "#/components/responses/McpServerResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: getMcpServer + description: Gets a single `McpServer` entity by ID. + parameters: + - name: server_id + description: A unique identifier for an `McpServer`. + schema: + type: string + in: path + required: true components: schemas: CatalogArtifact: @@ -610,6 +682,12 @@ components: type: array items: type: string + assetType: + $ref: "#/components/schemas/CatalogAssetType" + description: |- + The type of assets this source manages. Defaults to "models" if not specified. + This field determines which loader processes the source and which API endpoints + return the assets. status: $ref: "#/components/schemas/CatalogSourceStatus" description: Current operational status of the catalog source. @@ -804,7 +882,276 @@ components: - ID - NAME type: string - + CatalogAssetType: + description: |- + The type of assets managed by a catalog source. + - `models`: AI/ML models (default) + - `mcp_servers`: MCP (Model Context Protocol) servers + enum: + - models + - mcp_servers + type: string + default: models + McpServer: + description: An MCP (Model Context Protocol) server that provides tools for AI agents. + type: object + required: + - id + - name + properties: + id: + type: string + description: Unique identifier for the MCP server. + example: dynatrace-mcp + name: + type: string + description: Human-readable name of the MCP server. + example: Dynatrace MCP Server + source_id: + type: string + description: ID of the catalog source this server belongs to. + example: organization_mcp_servers + description: + type: string + description: Description of the MCP server and its capabilities. + logo: + type: string + format: uri + description: URL to the server's logo image. + license: + type: string + description: License identifier (SPDX format preferred) under which the MCP server is distributed. + example: Apache-2.0 + license_link: + type: string + format: uri + description: URL to the full license text or license file. + example: https://github.com/dynatrace-oss/dynatrace-mcp-server/blob/main/LICENSE + provider: + type: string + description: Organization or entity that provides the MCP server. + example: Dynatrace + version: + type: string + description: Version of the MCP server. + example: 1.0.1 + tags: + type: array + items: + type: string + description: Tags for categorizing and filtering MCP servers. + example: + - observability + - monitoring + - apm + tools: + type: array + items: + $ref: "#/components/schemas/McpTool" + description: List of tools exposed by this MCP server. + securityIndicators: + $ref: "#/components/schemas/McpSecurityIndicator" + documentationUrl: + type: string + format: uri + description: URL to the server's documentation. + repositoryUrl: + type: string + format: uri + description: URL to the server's source repository. + sourceCode: + type: string + description: Source code repository identifier (e.g., GitHub org/repo). + example: dynatrace-oss/dynatrace-mcp-server + lastUpdated: + type: string + format: date-time + description: When the server was last updated. + publishedDate: + type: string + format: date + description: When the server was first published. + artifacts: + type: array + items: + $ref: "#/components/schemas/McpArtifact" + description: Artifacts for this MCP server (e.g., OCI images for local deployments). + transports: + description: Supported transport types. For remote servers, this is derived from available endpoints. + type: array + items: + $ref: "#/components/schemas/McpTransportType" + readme: + type: string + description: Full README content in Markdown format. + deploymentMode: + $ref: "#/components/schemas/McpDeploymentMode" + endpoints: + $ref: "#/components/schemas/McpEndpoints" + customProperties: + description: |- + User provided custom properties which are not defined by its type. + Following the Model Registry pattern, tags are stored as MetadataStringValue + entries with empty string_value, and security indicators are stored as + MetadataBoolValue entries. + type: object + additionalProperties: + $ref: "#/components/schemas/MetadataValue" + McpServerList: + description: List of McpServer entities. + allOf: + - type: object + properties: + items: + description: Array of `McpServer` entities. + type: array + items: + $ref: "#/components/schemas/McpServer" + required: + - items + - $ref: "#/components/schemas/BaseResourceList" + McpTransportType: + description: |- + Transport protocol used by the MCP server. + - `stdio`: Standard input/output streams + - `sse`: Server-Sent Events over HTTP + - `http`: Standard HTTP/REST + enum: + - stdio + - sse + - http + type: string + McpDeploymentMode: + description: |- + Deployment mode for the MCP server. + - `local`: Server deployed from OCI artifact in Kubernetes cluster + - `remote`: Server hosted externally, accessed via network endpoints + enum: + - local + - remote + type: string + default: local + McpEndpoints: + description: |- + Network endpoints for remote MCP servers. Contains URLs for different + transport protocols. At least one endpoint must be provided for remote servers. + type: object + properties: + http: + type: string + format: uri + description: HTTP REST endpoint URL. + example: https://api.mcpservers.org/github-mcp/v1 + sse: + type: string + format: uri + description: Server-Sent Events endpoint URL. + example: https://api.mcpservers.org/github-mcp/sse + McpArtifact: + description: An artifact for an MCP server (e.g., OCI image for local deployment). + type: object + required: + - uri + properties: + uri: + type: string + format: uri + description: URI where the artifact can be retrieved (e.g., OCI image URI). + example: oci://ghcr.io/dynatrace-oss/dynatrace-mcp-server:1.0.1 + createTimeSinceEpoch: + type: string + description: Timestamp when the artifact was created (milliseconds since epoch). + lastUpdateTimeSinceEpoch: + type: string + description: Timestamp when the artifact was last updated (milliseconds since epoch). + McpTool: + description: A tool exposed by an MCP server. + type: object + required: + - name + - accessType + properties: + name: + type: string + description: Unique name of the tool within the MCP server. + example: execute_dql + description: + type: string + description: Description of what the tool does. + accessType: + $ref: "#/components/schemas/McpToolAccessType" + parameters: + type: array + items: + $ref: "#/components/schemas/McpToolParameter" + description: Parameters accepted by this tool. + revoked: + type: boolean + default: false + description: |- + Whether this tool has been revoked. Revoked tools should not be + invoked by AI agents. This allows for immediate disabling of + problematic tools without removing them from the registry. + revokedReason: + type: string + description: |- + Human-readable reason why the tool was revoked. This helps users + understand why the tool is unavailable and when it might be restored. + example: Security vulnerability CVE-2025-XXXX - pending patch + customProperties: + description: User provided custom properties for the tool. + type: object + additionalProperties: + $ref: "#/components/schemas/MetadataValue" + McpToolAccessType: + description: |- + Access type indicating what kind of operations the tool performs. + - `read_only`: Tool only reads data + - `read_write`: Tool can read and write data + - `execute`: Tool executes operations + enum: + - read_only + - read_write + - execute + type: string + McpToolParameter: + description: A parameter for an MCP tool. + type: object + required: + - name + - type + - required + properties: + name: + type: string + description: Name of the parameter. + example: query + type: + type: string + description: Data type of the parameter. + example: string + description: + type: string + description: Description of the parameter. + required: + type: boolean + description: Whether the parameter is required. + McpSecurityIndicator: + description: Security indicators for an MCP server. + type: object + properties: + verifiedSource: + type: boolean + description: Whether the source has been verified. + secureEndpoint: + type: boolean + description: Whether the endpoint uses secure communication. + sast: + type: boolean + description: Whether static application security testing has been performed. + readOnlyTools: + type: boolean + description: Whether all tools are read-only. responses: CatalogArtifactListResponse: content: @@ -945,14 +1292,25 @@ components: description: |- A response containing a list of models with their inclusion/exclusion status based on the provided catalog source configuration. - + McpServerListResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/McpServerList" + description: A response containing a list of McpServer entities. + McpServerResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/McpServer" + description: A response containing an `McpServer` entity. parameters: filterQuery: examples: filterQuery: value: "name='my-model' AND state='LIVE'" name: filterQuery - description: | + description: |- A SQL-like query string to filter catalog models. The query supports rich filtering capabilities with automatic type inference. **Supported Operators:** @@ -993,7 +1351,7 @@ components: artifactFilterQuery: value: "name='my-artifact' AND uri LIKE '%s3%'" name: filterQuery - description: | + description: |- A SQL-like query string to filter catalog artifacts. The query supports rich filtering capabilities with automatic type inference. **Supported Operators:** @@ -1054,7 +1412,7 @@ components: value: hardware_count.int_value summary: Order by custom integer property name: orderBy - description: | + description: |- Specifies the order by criteria for listing artifacts. **Standard Fields:** @@ -1096,7 +1454,7 @@ components: labelOrderBy: value: name name: orderBy - description: | + description: |- Specifies the key to order catalog labels by. You can provide any string key that may exist in the label maps. Labels that contain the specified key will be sorted by that key's value. Labels that don't contain the key will maintain @@ -1134,4 +1492,54 @@ components: $ref: "#/components/schemas/ArtifactTypeQueryParam" in: query required: false + assetType: + name: assetType + description: |- + Filter sources by asset type. + - `models`: Sources containing AI/ML models + - `mcp_servers`: Sources containing MCP (Model Context Protocol) servers + schema: + $ref: "#/components/schemas/CatalogAssetType" + in: query + required: false + namedQuery: + name: namedQuery + description: |- + Apply a pre-defined named query to filter the list of entities. Named queries + are configured in the catalog sources YAML file and provide reusable filter + presets for common filtering scenarios. + + **Configuration Example:** + ```yaml + namedQueries: + production_ready: + provider: + operator: "IN" + value: ["Dynatrace", "CNCF", "GitHub"] + verifiedSource: + operator: "=" + value: true + monitoring_tools: + deploymentMode: + operator: "=" + value: "local" + ``` + + **Usage:** + - Apply single named query: `?namedQuery=production_ready` + - Named queries work alongside other filters (filterQuery, name) + - If the named query doesn't exist, a 400 Bad Request is returned + + **Behavior:** + - Named query filters are applied in addition to any other filters + - Named queries can reference customProperties and standard fields + - Results must satisfy ALL conditions in the named query + schema: + type: string + in: query + required: false + examples: + namedQuery: + value: production_ready + summary: Apply production_ready named query tags: [] diff --git a/catalog/cmd/catalog.go b/catalog/cmd/catalog.go index 5e4a5b9e01..ff81daeb36 100644 --- a/catalog/cmd/catalog.go +++ b/catalog/cmd/catalog.go @@ -11,7 +11,9 @@ import ( "github.com/kubeflow/model-registry/catalog/internal/catalog" "github.com/kubeflow/model-registry/catalog/internal/db/models" "github.com/kubeflow/model-registry/catalog/internal/db/service" + "github.com/kubeflow/model-registry/catalog/internal/mcp" "github.com/kubeflow/model-registry/catalog/internal/server/openapi" + openapimodel "github.com/kubeflow/model-registry/catalog/pkg/openapi" "github.com/kubeflow/model-registry/internal/datastore" "github.com/kubeflow/model-registry/internal/datastore/embedmd" "github.com/spf13/cobra" @@ -20,10 +22,12 @@ import ( var catalogCfg = struct { ListenAddress string ConfigPath []string + McpCatalogPath []string PerformanceMetricsPath []string }{ ListenAddress: "0.0.0.0:8080", ConfigPath: []string{"sources.yaml"}, + McpCatalogPath: []string{}, PerformanceMetricsPath: []string{}, } @@ -41,6 +45,7 @@ func init() { fs := CatalogCmd.Flags() fs.StringVarP(&catalogCfg.ListenAddress, "listen", "l", catalogCfg.ListenAddress, "Address to listen on") fs.StringSliceVar(&catalogCfg.ConfigPath, "catalogs-path", catalogCfg.ConfigPath, "Path to catalog source configuration file") + fs.StringSliceVar(&catalogCfg.McpCatalogPath, "mcp-catalogs-path", catalogCfg.McpCatalogPath, "Path to MCP catalog source configuration file") fs.StringSliceVar(&catalogCfg.PerformanceMetricsPath, "performance-metrics", catalogCfg.PerformanceMetricsPath, "Path to performance metrics data directory") } @@ -65,6 +70,7 @@ func runCatalogServer(cmd *cobra.Command, args []string) error { getRepo[models.CatalogMetricsArtifactRepository](repoSet), getRepo[models.CatalogSourceRepository](repoSet), getRepo[models.PropertyOptionsRepository](repoSet), + getRepo[models.McpServerRepository](repoSet), ) loader := catalog.NewLoader(services, catalogCfg.ConfigPath) @@ -94,8 +100,45 @@ func runCatalogServer(cmd *cobra.Command, args []string) error { ) ctrl := openapi.NewModelCatalogServiceAPIController(svc) + // Initialize MCP Catalog service + // Always use database-backed provider - if no sources configured, returns empty list + var mcpLoader *catalog.McpLoader + if len(catalogCfg.McpCatalogPath) > 0 { + // Load MCP servers from YAML sources into database + // Pass the shared SourceCollection so MCP sources appear in unified /sources API + mcpLoader = catalog.NewMcpLoader(services, catalogCfg.McpCatalogPath, loader.Sources) + err = mcpLoader.Start(context.Background()) + if err != nil { + return fmt.Errorf("error loading MCP catalog sources: %v", err) + } + glog.Infof("MCP catalog loaded from %d source(s)", len(catalogCfg.McpCatalogPath)) + } else { + glog.Infof("No MCP catalog sources configured (use --mcp-catalogs-path to specify)") + } + mcpProvider := mcp.NewDbMcpCatalogProvider(services.McpServerRepository) + // Set named query resolver from MCP loader (or shared SourceCollection) + if mcpLoader != nil { + mcpProvider.SetNamedQueryResolver(func() map[string]map[string]openapimodel.FieldFilter { + namedQueries := mcpLoader.GetNamedQueries() + // Convert catalog.FieldFilter to openapimodel.FieldFilter + result := make(map[string]map[string]openapimodel.FieldFilter, len(namedQueries)) + for queryName, fieldFilters := range namedQueries { + result[queryName] = make(map[string]openapimodel.FieldFilter, len(fieldFilters)) + for fieldName, filter := range fieldFilters { + result[queryName][fieldName] = openapimodel.FieldFilter{ + Operator: filter.Operator, + Value: filter.Value, + } + } + } + return result + }) + } + mcpSvc := openapi.NewMcpCatalogServiceAPIService(mcpProvider) + mcpCtrl := openapi.NewMcpCatalogServiceAPIController(mcpSvc) + glog.Infof("Catalog API server listening on %s", catalogCfg.ListenAddress) - return http.ListenAndServe(catalogCfg.ListenAddress, openapi.NewRouter(ctrl)) + return http.ListenAndServe(catalogCfg.ListenAddress, openapi.NewRouter(ctrl, mcpCtrl)) } func getRepo[T any](repoSet datastore.RepoSet) T { diff --git a/catalog/internal/catalog/asset_types.go b/catalog/internal/catalog/asset_types.go new file mode 100644 index 0000000000..716c5872c0 --- /dev/null +++ b/catalog/internal/catalog/asset_types.go @@ -0,0 +1,17 @@ +package catalog + +import ( + "github.com/kubeflow/model-registry/catalog/internal/common" +) + +// AssetType is an alias to common.AssetType for convenience +type AssetType = common.AssetType + +// Re-export constants from common package +const ( + AssetTypeModels = common.AssetTypeModels + AssetTypeMcpServers = common.AssetTypeMcpServers +) + +// SourceProperties is an alias to common.SourceProperties +type SourceProperties = common.SourceProperties diff --git a/catalog/internal/catalog/catalog_test.go b/catalog/internal/catalog/catalog_test.go index 8519873492..dd3a29bdbc 100644 --- a/catalog/internal/catalog/catalog_test.go +++ b/catalog/internal/catalog/catalog_test.go @@ -43,6 +43,7 @@ func TestLoadCatalogSources(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) loader := NewLoader(services, []string{tt.args.catalogsPath}) err := loader.Start(context.Background()) @@ -65,6 +66,7 @@ func TestLoadCatalogSources(t *testing.T) { func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) { trueValue := true falseValue := false + modelsAssetType := apimodels.CATALOGASSETTYPE_MODELS type args struct { catalogsPath string } @@ -79,16 +81,18 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) { args: args{catalogsPath: "testdata/test-catalog-sources.yaml"}, want: map[string]apimodels.CatalogSource{ "catalog1": { - Id: "catalog1", - Name: "Catalog 1", - Enabled: &trueValue, - Labels: []string{}, + Id: "catalog1", + Name: "Catalog 1", + Enabled: &trueValue, + Labels: []string{}, + AssetType: &modelsAssetType, }, "catalog2": { - Id: "catalog2", - Name: "Catalog 2", - Enabled: &falseValue, - Labels: []string{}, + Id: "catalog2", + Name: "Catalog 2", + Enabled: &falseValue, + Labels: []string{}, + AssetType: &modelsAssetType, }, }, wantErr: false, @@ -104,6 +108,7 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) loader := NewLoader(services, []string{tt.args.catalogsPath}) err := loader.Start(context.Background()) @@ -131,6 +136,7 @@ func TestLabelsValidation(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) tests := []struct { @@ -262,6 +268,7 @@ func TestCatalogSourceLabelsDefaultToEmptySlice(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) loader := NewLoader(services, []string{tt.args.catalogsPath}) err := loader.Start(context.Background()) @@ -301,6 +308,7 @@ func TestLoadCatalogSourcesWithMockRepositories(t *testing.T) { mockMetricsArtifactRepo, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) // Register a test provider that will create some test data @@ -431,6 +439,7 @@ func TestLoadCatalogSourcesWithRepositoryErrors(t *testing.T) { mockMetricsArtifactRepo, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) // Register a test provider @@ -509,6 +518,7 @@ func TestLoadCatalogSourcesWithNilEnabled(t *testing.T) { mockMetricsArtifactRepo, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) // Register a test provider @@ -1015,6 +1025,7 @@ func TestAPIProviderGetPerformanceArtifacts(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) provider := NewDBCatalog(services, nil) @@ -1043,6 +1054,7 @@ func TestAPIProviderInterface(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) var provider APIProvider = NewDBCatalog(services, nil) diff --git a/catalog/internal/catalog/db_catalog_test.go b/catalog/internal/catalog/db_catalog_test.go index f972d0aad9..f0c4ce2d06 100644 --- a/catalog/internal/catalog/db_catalog_test.go +++ b/catalog/internal/catalog/db_catalog_test.go @@ -52,6 +52,7 @@ func TestDBCatalog(t *testing.T) { metricsArtifactRepo, catalogSourceRepo, service.NewPropertyOptionsRepository(sharedDB), + nil, // McpServerRepository ) // Create DB catalog instance @@ -1437,6 +1438,7 @@ func TestDBCatalog_GetPerformanceArtifactsWithService(t *testing.T) { metricsArtifactRepo, catalogSourceRepo, service.NewPropertyOptionsRepository(sharedDB), + nil, // McpServerRepository ) sources := NewSourceCollection() diff --git a/catalog/internal/catalog/loader.go b/catalog/internal/catalog/loader.go index 9bf4039640..43298300f1 100644 --- a/catalog/internal/catalog/loader.go +++ b/catalog/internal/catalog/loader.go @@ -79,6 +79,20 @@ type Source struct { // This is set automatically during loading and used for resolving relative paths. // It is not read from YAML; it's set programmatically. Origin string `json:"-" yaml:"-"` + + // DetectedAssetType is the type of assets contained in this source. + // It is detected from the YAML content (models: or mcp_servers: key) and not read from YAML. + DetectedAssetType AssetType `json:"-" yaml:"-"` +} + +// GetId returns the source ID. Implements common.SourceProperties interface. +func (s *Source) GetId() string { + return s.Id +} + +// GetProperties returns the source properties map. Implements common.SourceProperties interface. +func (s *Source) GetProperties() map[string]any { + return s.Properties } type Loader struct { diff --git a/catalog/internal/catalog/loader_test.go b/catalog/internal/catalog/loader_test.go index 1603d20cc5..43fc3d0f0e 100644 --- a/catalog/internal/catalog/loader_test.go +++ b/catalog/internal/catalog/loader_test.go @@ -105,6 +105,7 @@ func TestRemoveModelsFromMissingSources(t *testing.T) { &MockCatalogMetricsArtifactRepository{}, &MockCatalogSourceRepository{}, &MockPropertyOptionsRepository{}, + nil, // McpServerRepository ) // Create loader and populate sources diff --git a/catalog/internal/catalog/mcp_loader.go b/catalog/internal/catalog/mcp_loader.go new file mode 100644 index 0000000000..b578f3ca7b --- /dev/null +++ b/catalog/internal/catalog/mcp_loader.go @@ -0,0 +1,509 @@ +package catalog + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/golang/glog" + dbmodels "github.com/kubeflow/model-registry/catalog/internal/db/models" + "github.com/kubeflow/model-registry/catalog/internal/db/service" + "github.com/kubeflow/model-registry/catalog/internal/mcp" + model "github.com/kubeflow/model-registry/catalog/pkg/openapi" + mrmodels "github.com/kubeflow/model-registry/internal/db/models" + "k8s.io/apimachinery/pkg/util/yaml" +) + +// McpLoaderEventHandler is the definition of a function called after an MCP server is loaded. +type McpLoaderEventHandler func(ctx context.Context, record mcp.McpServerProviderRecord) error + +// mcpSourceConfig is the structure for the MCP catalog sources YAML file. +type mcpSourceConfig struct { + Catalogs []mcp.McpSource `json:"catalogs" yaml:"catalogs"` + NamedQueries map[string]map[string]FieldFilter `json:"namedQueries,omitempty" yaml:"namedQueries,omitempty"` +} + +// McpLoader handles loading MCP servers from YAML sources into the database. +type McpLoader struct { + paths []string + services service.Services + sources *SourceCollection // shared source collection for unified sources API + closersMu sync.Mutex + closer func() // cancels the current MCP loading goroutines + handlers []McpLoaderEventHandler + loadedSources map[string]bool // tracks which source IDs have been loaded + namedQueries map[string]map[string]FieldFilter // merged named queries from all config files +} + +// NewMcpLoader creates a new MCP loader. +func NewMcpLoader(services service.Services, paths []string, sources *SourceCollection) *McpLoader { + // Convert paths to absolute for consistent origin ordering. + absPaths := make([]string, 0, len(paths)) + for _, p := range paths { + absPath, err := filepath.Abs(p) + if err != nil { + // Fall back to original path if conversion fails + absPath = p + } + absPaths = append(absPaths, absPath) + } + + return &McpLoader{ + paths: absPaths, + services: services, + sources: sources, + loadedSources: map[string]bool{}, + namedQueries: make(map[string]map[string]FieldFilter), + } +} + +// RegisterEventHandler adds a function that will be called for every +// successfully processed record. This should be called before Start. +func (l *McpLoader) RegisterEventHandler(fn McpLoaderEventHandler) { + l.handlers = append(l.handlers, fn) +} + +// Start processes the MCP sources YAML files. Background goroutines will be +// stopped when the context is canceled. +func (l *McpLoader) Start(ctx context.Context) error { + // Phase 1: Parse all config files and merge sources with field-level priority. + // Sources from later config files override fields from earlier ones. + allSources, err := l.readAndMergeSources() + if err != nil { + return fmt.Errorf("failed to read MCP sources: %w", err) + } + + // Phase 1.5: Read and merge named queries from all config files + l.readAndMergeNamedQueries() + + if len(allSources) == 0 { + glog.Infof("No MCP catalog sources found") + return nil + } + + // Delete MCP servers from unknown or disabled sources + err = l.removeMcpServersFromMissingSources(allSources) + if err != nil { + return fmt.Errorf("failed to remove MCP servers from missing sources: %w", err) + } + + // Merge MCP sources and named queries into the shared SourceCollection for unified /sources API + l.mergeMcpSourcesIntoCollection(allSources) + + // Phase 2: Load MCP servers from sources + err = l.loadAllMcpServers(ctx, allSources) + if err != nil { + return err + } + + // Phase 3: Set up file watchers for hot-reload + for _, path := range l.paths { + go func(path string) { + changes, err := getMonitor().Path(ctx, path) + if err != nil { + glog.Errorf("unable to watch MCP sources file (%s): %v", path, err) + return + } + + for range changes { + glog.Infof("Reloading MCP sources %s", path) + l.reloadAll(ctx) + } + }(path) + } + + return nil +} + +// read reads an MCP source configuration file. +func (l *McpLoader) read(path string) ([]mcp.McpSource, error) { + bytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + config := &mcpSourceConfig{} + if err = yaml.UnmarshalStrict(bytes, &config); err != nil { + return nil, err + } + + return config.Catalogs, nil +} + +// readConfig reads an MCP source configuration file and returns both sources and named queries. +func (l *McpLoader) readConfig(path string) (*mcpSourceConfig, error) { + bytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + config := &mcpSourceConfig{} + if err = yaml.UnmarshalStrict(bytes, &config); err != nil { + return nil, err + } + + return config, nil +} + +// loadAllMcpServers loads MCP servers from all sources. +func (l *McpLoader) loadAllMcpServers(ctx context.Context, sources map[string]mcp.McpSource) error { + // Clear the loaded sources tracker for a fresh load + l.loadedSources = map[string]bool{} + + return l.updateDatabase(ctx, sources) +} + +// reloadAll re-parses all config files and reloads all MCP servers. +func (l *McpLoader) reloadAll(ctx context.Context) { + // Use the same merge logic as Start() + allSources, err := l.readAndMergeSources() + if err != nil { + glog.Errorf("unable to read and merge MCP sources: %v", err) + return + } + + // Re-read named queries on reload + l.readAndMergeNamedQueries() + + // Merge into shared SourceCollection for unified /sources API + l.mergeMcpSourcesIntoCollection(allSources) + + // Clean up MCP servers that are no longer in config + if err := l.removeMcpServersFromMissingSources(allSources); err != nil { + glog.Errorf("unable to remove MCP servers from missing sources: %v", err) + } + + // Reload all MCP servers + if err := l.loadAllMcpServers(ctx, allSources); err != nil { + glog.Errorf("unable to reload MCP servers: %v", err) + } +} + +// readAndMergeSources reads MCP sources from all config paths and merges sources +// with the same ID using field-level priority. Sources from later paths override +// fields from earlier paths. +func (l *McpLoader) readAndMergeSources() (map[string]mcp.McpSource, error) { + return MergeMcpSourcesFromPaths(l.paths, l.readWithWarning) +} + +// readAndMergeNamedQueries reads named queries from all config files and merges them. +// Named queries from later paths override those from earlier paths. +func (l *McpLoader) readAndMergeNamedQueries() { + // Clear existing named queries + l.namedQueries = make(map[string]map[string]FieldFilter) + + // Read from all paths in priority order (later paths override earlier) + for _, path := range l.paths { + config, err := l.readConfig(path) + if err != nil { + glog.V(2).Infof("Skipping named queries from %s: %v", path, err) + continue + } + + if config.NamedQueries == nil { + continue + } + + // Merge named queries (later files override earlier ones) + for queryName, fieldFilters := range config.NamedQueries { + if l.namedQueries[queryName] == nil { + l.namedQueries[queryName] = make(map[string]FieldFilter) + } + for fieldName, filter := range fieldFilters { + l.namedQueries[queryName][fieldName] = filter + } + } + } + + if len(l.namedQueries) > 0 { + glog.Infof("Loaded %d MCP named queries", len(l.namedQueries)) + } +} + +// GetNamedQueries returns all merged named queries for MCP servers. +func (l *McpLoader) GetNamedQueries() map[string]map[string]FieldFilter { + // Return a copy to prevent external modification + result := make(map[string]map[string]FieldFilter, len(l.namedQueries)) + for queryName, fieldFilters := range l.namedQueries { + result[queryName] = make(map[string]FieldFilter, len(fieldFilters)) + for fieldName, filter := range fieldFilters { + result[queryName][fieldName] = filter + } + } + return result +} + +// readWithWarning reads an MCP source configuration file and logs warnings on failure. +func (l *McpLoader) readWithWarning(path string) ([]mcp.McpSource, error) { + sources, err := l.read(path) + if err != nil { + glog.Warningf("MCP catalog source file %s not found or invalid: %v", path, err) + return nil, err + } + return sources, nil +} + +// updateDatabase loads MCP servers into the database. +func (l *McpLoader) updateDatabase(ctx context.Context, sources map[string]mcp.McpSource) error { + ctx, cancel := context.WithCancel(ctx) + + l.closersMu.Lock() + if l.closer != nil { + l.closer() + } + l.closer = cancel + l.closersMu.Unlock() + + records := l.readProviderRecords(ctx, sources) + + go func() { + for record := range records { + if record.Server == nil { + continue + } + attr := record.Server.GetAttributes() + if attr == nil || attr.Name == nil { + continue + } + + glog.Infof("Loading MCP server %s with %d tool(s)", *attr.Name, len(record.Tools)) + + _, err := l.services.McpServerRepository.Save(record.Server) + if err != nil { + glog.Errorf("%s: unable to save MCP server: %v", *attr.Name, err) + continue + } + + for _, handler := range l.handlers { + handler(ctx, record) + } + } + }() + + return nil +} + +// readProviderRecords calls the provider for every source and merges the returned channels together. +func (l *McpLoader) readProviderRecords(ctx context.Context, sources map[string]mcp.McpSource) <-chan mcp.McpServerProviderRecord { + ch := make(chan mcp.McpServerProviderRecord) + var wg sync.WaitGroup + + for _, source := range sources { + // Skip disabled sources + if source.Enabled != nil && !*source.Enabled { + continue + } + + // Skip sources that have already been loaded + if l.loadedSources[source.Id] { + continue + } + + if source.Type == "" { + glog.Errorf("MCP source %s has no type defined, skipping", source.Id) + continue + } + + // Mark this source as loaded + l.loadedSources[source.Id] = true + + // Build the server filter for this source + serverFilter, err := NewMcpServerFilterFromSource(&source) + if err != nil { + glog.Errorf("MCP source %s has invalid filter configuration: %v", source.Id, err) + continue + } + + glog.Infof("Reading MCP servers from %s source %s", source.Type, source.Id) + + providerFunc, ok := mcp.RegisteredMcpProviders[source.Type] + if !ok { + glog.Errorf("MCP catalog type %s not registered", source.Type) + continue + } + + // Use the source's origin directory for resolving relative paths. + sourceDir := filepath.Dir(source.Origin) + sourceCopy := source + + records, err := providerFunc(ctx, &sourceCopy, sourceDir) + if err != nil { + glog.Errorf("error reading MCP catalog type %s with id %s: %v", source.Type, source.Id, err) + continue + } + + wg.Add(1) + go func(ctx context.Context, sourceID string, filter *McpServerFilter) { + defer wg.Done() + + serverNames := []string{} + + for r := range records { + if r.Server == nil { + glog.V(2).Infof("%s: MCP servers batch complete", sourceID) + + // Copy the list of server names, then clear it. + serverNameSet := mapset.NewSet(serverNames...) + serverNames = serverNames[:0] + + go func() { + err := l.removeOrphanedMcpServersFromSource(sourceID, serverNameSet) + if err != nil { + glog.Errorf("error removing orphaned MCP servers: %v", err) + } + }() + continue + } + + // Apply server filter + attr := r.Server.GetAttributes() + if attr != nil && attr.Name != nil { + serverName := *attr.Name + if !filter.Allows(serverName) { + glog.V(2).Infof("%s: MCP server %s excluded by filter", sourceID, serverName) + continue + } + serverNames = append(serverNames, serverName) + } + + // Set source_id on every returned server. + l.setMcpServerSourceID(r.Server, sourceID) + + ch <- r + } + }(ctx, source.Id, serverFilter) + } + + go func() { + defer close(ch) + wg.Wait() + }() + + return ch +} + +// setMcpServerSourceID adds the source_id property to an MCP server. +func (l *McpLoader) setMcpServerSourceID(server dbmodels.McpServer, sourceID string) { + if server == nil { + return + } + + props := server.GetProperties() + if props == nil { + if serverImpl, ok := server.(*dbmodels.McpServerImpl); ok { + newProps := make([]mrmodels.Properties, 0, 1) + serverImpl.Properties = &newProps + props = &newProps + } else { + return + } + } + + for i := range *props { + if (*props)[i].Name == "source_id" { + // Already has a source_id, just update it + (*props)[i].StringValue = &sourceID + return + } + } + + *props = append(*props, mrmodels.NewStringProperty("source_id", sourceID, false)) +} + +// removeMcpServersFromMissingSources removes MCP servers from sources that are no longer in config or disabled. +func (l *McpLoader) removeMcpServersFromMissingSources(sources map[string]mcp.McpSource) error { + enabledSourceIDs := mapset.NewSet[string]() + for id, source := range sources { + if source.Enabled == nil || *source.Enabled { + enabledSourceIDs.Add(id) + } + } + + existingSourceIDs, err := l.services.McpServerRepository.GetDistinctSourceIDs() + if err != nil { + return fmt.Errorf("unable to retrieve existing MCP source IDs: %w", err) + } + + for oldSource := range mapset.NewSet(existingSourceIDs...).Difference(enabledSourceIDs).Iter() { + glog.Infof("Removing MCP servers from source %s", oldSource) + + err = l.services.McpServerRepository.DeleteBySource(oldSource) + if err != nil { + return fmt.Errorf("unable to remove MCP servers from source %q: %w", oldSource, err) + } + } + + return nil +} + +// removeOrphanedMcpServersFromSource removes MCP servers that are no longer in the source. +func (l *McpLoader) removeOrphanedMcpServersFromSource(sourceID string, valid mapset.Set[string]) error { + list, err := l.services.McpServerRepository.List(dbmodels.McpServerListOptions{ + SourceIDs: &[]string{sourceID}, + }) + if err != nil { + return fmt.Errorf("unable to list MCP servers from source %q: %w", sourceID, err) + } + + for _, server := range list.Items { + attr := server.GetAttributes() + if attr == nil || attr.Name == nil || server.GetID() == nil { + continue + } + + if valid.Contains(*attr.Name) { + continue + } + + glog.Infof("Removing %s MCP server %s", sourceID, *attr.Name) + + err = l.services.McpServerRepository.DeleteByID(*server.GetID()) + if err != nil { + return fmt.Errorf("unable to remove MCP server %d (%s from source %s): %w", *server.GetID(), *attr.Name, sourceID, err) + } + } + + return nil +} + +// mergeMcpSourcesIntoCollection converts MCP sources to catalog Sources and merges them +// into the shared SourceCollection. This enables the unified /sources API to return +// MCP sources with assetType=mcp_servers. It also merges MCP named queries. +func (l *McpLoader) mergeMcpSourcesIntoCollection(mcpSources map[string]mcp.McpSource) { + if l.sources == nil { + return + } + + // Convert MCP sources to catalog Sources + catalogSources := make(map[string]Source) + for id, mcpSource := range mcpSources { + enabled := mcpSource.Enabled + if enabled == nil { + defaultEnabled := true + enabled = &defaultEnabled + } + + catalogSources[id] = Source{ + CatalogSource: model.CatalogSource{ + Id: mcpSource.Id, + Name: mcpSource.Name, + Labels: mcpSource.Labels, + Enabled: enabled, + }, + Type: mcpSource.Type, + Properties: mcpSource.Properties, + Origin: mcpSource.Origin, + DetectedAssetType: AssetTypeMcpServers, + } + } + + // Merge sources and named queries into the shared source collection + if len(l.paths) > 0 { + if err := l.sources.MergeWithNamedQueries(l.paths[0], catalogSources, l.namedQueries); err != nil { + glog.Errorf("unable to merge MCP sources into source collection: %v", err) + } + } +} diff --git a/catalog/internal/catalog/mcp_server_filter.go b/catalog/internal/catalog/mcp_server_filter.go new file mode 100644 index 0000000000..9a01d28031 --- /dev/null +++ b/catalog/internal/catalog/mcp_server_filter.go @@ -0,0 +1,124 @@ +package catalog + +import ( + "fmt" + "strings" + + "github.com/kubeflow/model-registry/catalog/internal/mcp" +) + +// McpServerFilter encapsulates include/exclude pattern matching for MCP server names. +// It reuses the same compiledPattern type from model_filter.go for consistency. +type McpServerFilter struct { + included []*compiledPattern + excluded []*compiledPattern +} + +// ValidateMcpServerSourceFilters validates that the includedServers and excludedServers patterns +// are valid (non-empty, compilable, non-conflicting). This is useful for early validation +// at configuration load time without constructing the full McpServerFilter. +func ValidateMcpServerSourceFilters(included, excluded []string) error { + if err := detectConflictingMcpServerPatterns(included, excluded); err != nil { + return err + } + + if _, err := compilePatterns("includedServers", included); err != nil { + return err + } + + if _, err := compilePatterns("excludedServers", excluded); err != nil { + return err + } + + return nil +} + +// NewMcpServerFilter builds a McpServerFilter from the provided include/exclude pattern lists. +func NewMcpServerFilter(included, excluded []string) (*McpServerFilter, error) { + if err := ValidateMcpServerSourceFilters(included, excluded); err != nil { + return nil, err + } + + inc, err := compilePatterns("includedServers", included) + if err != nil { + return nil, err + } + + exc, err := compilePatterns("excludedServers", excluded) + if err != nil { + return nil, err + } + + if len(inc) == 0 && len(exc) == 0 { + return nil, nil + } + + return &McpServerFilter{ + included: inc, + excluded: exc, + }, nil +} + +// detectConflictingMcpServerPatterns checks if any pattern appears in both included and excluded lists. +func detectConflictingMcpServerPatterns(included, excluded []string) error { + if len(included) == 0 || len(excluded) == 0 { + return nil + } + + includedIdx := make(map[string]int, len(included)) + for i, pattern := range included { + value := strings.TrimSpace(pattern) + includedIdx[value] = i + } + + for j, pattern := range excluded { + value := strings.TrimSpace(pattern) + if i, exists := includedIdx[value]; exists { + return fmt.Errorf("pattern %q is defined in both includedServers[%d] and excludedServers[%d]", value, i, j) + } + } + return nil +} + +// Allows returns true if the provided MCP server name passes the include/exclude rules. +func (f *McpServerFilter) Allows(name string) bool { + if f == nil { + return true + } + + if len(f.included) > 0 { + matched := false + for _, pattern := range f.included { + if pattern.re.MatchString(name) { + matched = true + break + } + } + if !matched { + return false + } + } + + for _, pattern := range f.excluded { + if pattern.re.MatchString(name) { + return false + } + } + + return true +} + +// NewMcpServerFilterFromSource composes a McpServerFilter using the source-level configuration. +func NewMcpServerFilterFromSource(source *mcp.McpSource) (*McpServerFilter, error) { + if source == nil { + return nil, fmt.Errorf("source cannot be nil when building filters") + } + + filter, err := NewMcpServerFilter(source.IncludedServers, source.ExcludedServers) + if err != nil { + return nil, fmt.Errorf("invalid include/exclude configuration for MCP source %s: %w", source.Id, err) + } + + return filter, nil +} + diff --git a/catalog/internal/catalog/mcp_server_filter_test.go b/catalog/internal/catalog/mcp_server_filter_test.go new file mode 100644 index 0000000000..2c2e92a9d6 --- /dev/null +++ b/catalog/internal/catalog/mcp_server_filter_test.go @@ -0,0 +1,173 @@ +package catalog + +import ( + "testing" + + "github.com/kubeflow/model-registry/catalog/internal/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMcpServerFilterAllows(t *testing.T) { + filter, err := NewMcpServerFilter([]string{"github/*"}, []string{"github/beta-*"}) + require.NoError(t, err) + + assert.True(t, filter.Allows("github/filesystem")) + assert.True(t, filter.Allows("github/fetch")) + assert.False(t, filter.Allows("github/beta-server")) + assert.False(t, filter.Allows("other/mcp-server")) + + // Test case-insensitive matching + assert.True(t, filter.Allows("GitHub/filesystem")) + assert.True(t, filter.Allows("GITHUB/filesystem")) + assert.False(t, filter.Allows("GITHUB/BETA-server")) + + allowAll, err := NewMcpServerFilter([]string{"*"}, nil) + require.NoError(t, err) + assert.True(t, allowAll.Allows("anything/goes")) +} + +func TestMcpServerFilterNilReturnsNil(t *testing.T) { + // When no filters are provided, NewMcpServerFilter returns nil, nil + filter, err := NewMcpServerFilter(nil, nil) + require.NoError(t, err) + assert.Nil(t, filter) + + // A nil filter should allow everything + assert.True(t, (*McpServerFilter)(nil).Allows("any-server")) +} + +func TestMcpServerFilterExcludeOnly(t *testing.T) { + // Test with only exclusions - should allow all except excluded patterns + filter, err := NewMcpServerFilter(nil, []string{"*-deprecated", "*-legacy"}) + require.NoError(t, err) + + assert.True(t, filter.Allows("github-filesystem")) + assert.True(t, filter.Allows("slack-mcp")) + assert.False(t, filter.Allows("old-server-deprecated")) + assert.False(t, filter.Allows("v1-legacy")) +} + +func TestMcpServerFilterIncludeOnly(t *testing.T) { + // Test with only inclusions - should only allow matching patterns + filter, err := NewMcpServerFilter([]string{"github-*", "slack-*"}, nil) + require.NoError(t, err) + + assert.True(t, filter.Allows("github-filesystem")) + assert.True(t, filter.Allows("slack-mcp")) + assert.False(t, filter.Allows("other-server")) + assert.False(t, filter.Allows("custom-mcp")) +} + +func TestMcpServerFilterConflictsAndValidation(t *testing.T) { + _, err := NewMcpServerFilter([]string{"github/*"}, []string{"github/*"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "pattern \"github/*\"") + + _, err = NewMcpServerFilter([]string{""}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "pattern cannot be empty") +} + +func TestMcpServerFilterFromSource(t *testing.T) { + t.Run("source with filters", func(t *testing.T) { + source := &mcp.McpSource{ + Id: "test-source", + Name: "Test MCP Source", + IncludedServers: []string{"github-*", "slack-*"}, + ExcludedServers: []string{"*-deprecated"}, + } + + filter, err := NewMcpServerFilterFromSource(source) + require.NoError(t, err) + require.NotNil(t, filter) + + assert.True(t, filter.Allows("github-filesystem")) + assert.True(t, filter.Allows("slack-mcp")) + assert.False(t, filter.Allows("other-server")) + assert.False(t, filter.Allows("github-deprecated")) + }) + + t.Run("source without filters", func(t *testing.T) { + source := &mcp.McpSource{ + Id: "test-source", + Name: "Test MCP Source", + } + + filter, err := NewMcpServerFilterFromSource(source) + require.NoError(t, err) + assert.Nil(t, filter) // No filters means nil filter (allow all) + }) + + t.Run("nil source", func(t *testing.T) { + _, err := NewMcpServerFilterFromSource(nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "source cannot be nil") + }) + + t.Run("source with invalid patterns", func(t *testing.T) { + source := &mcp.McpSource{ + Id: "test-source", + Name: "Test MCP Source", + IncludedServers: []string{""}, + } + + _, err := NewMcpServerFilterFromSource(source) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid include/exclude configuration") + }) +} + +func TestMcpServerFilterWithWildcardInMiddle(t *testing.T) { + // Test that wildcards match across the entire name + filter, err := NewMcpServerFilter(nil, []string{"*deprecated*", "*old*"}) + require.NoError(t, err) + + assert.True(t, filter.Allows("github-filesystem")) + assert.False(t, filter.Allows("server-deprecated-v1")) + assert.False(t, filter.Allows("old-server")) + assert.False(t, filter.Allows("my-old-mcp")) + + // Test that */pattern* requires the pattern immediately after / + filter2, err := NewMcpServerFilter(nil, []string{"*/deprecated", "*/old*"}) + require.NoError(t, err) + + assert.True(t, filter2.Allows("foo/bar-deprecated")) // doesn't match */deprecated + assert.False(t, filter2.Allows("foo/deprecated")) // matches */deprecated + assert.False(t, filter2.Allows("bar/old-server")) // matches */old* +} + +func TestValidateMcpServerSourceFilters(t *testing.T) { + t.Run("no filters", func(t *testing.T) { + err := ValidateMcpServerSourceFilters(nil, nil) + assert.NoError(t, err) + }) + + t.Run("valid patterns", func(t *testing.T) { + err := ValidateMcpServerSourceFilters([]string{"github-*", "slack-*"}, []string{"*-beta"}) + assert.NoError(t, err) + }) + + t.Run("conflicting patterns", func(t *testing.T) { + err := ValidateMcpServerSourceFilters([]string{"github-*"}, []string{"github-*"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "github-*") + }) + + t.Run("empty pattern in includedServers", func(t *testing.T) { + err := ValidateMcpServerSourceFilters([]string{"github-*", ""}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "pattern cannot be empty") + }) + + t.Run("whitespace-only pattern", func(t *testing.T) { + err := ValidateMcpServerSourceFilters([]string{" "}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "pattern cannot be empty") + }) + + t.Run("valid glob patterns", func(t *testing.T) { + err := ValidateMcpServerSourceFilters([]string{"valid-*"}, nil) + assert.NoError(t, err) + }) +} diff --git a/catalog/internal/catalog/mcp_source_merge.go b/catalog/internal/catalog/mcp_source_merge.go new file mode 100644 index 0000000000..b1c2f10a4d --- /dev/null +++ b/catalog/internal/catalog/mcp_source_merge.go @@ -0,0 +1,131 @@ +package catalog + +import ( + "github.com/kubeflow/model-registry/catalog/internal/mcp" +) + +// mergeMcpSources performs field-level merging of two McpSource structs. +// Fields from 'override' take precedence over 'base' when they are explicitly set. +// A field is considered "set" if: +// - For strings: non-empty +// - For pointers: non-nil +// - For slices: non-nil (empty slice is considered explicitly set to "no items") +// - For maps: non-nil (empty map is considered explicitly set) +// +// This follows the same pattern as mergeSources() for model catalog sources. +func mergeMcpSources(base, override mcp.McpSource) mcp.McpSource { + result := base + + // Id is always taken from override (it's the key) + result.Id = override.Id + + // Name: override if non-empty + if override.Name != "" { + result.Name = override.Name + } + + // Enabled: override if non-nil + if override.Enabled != nil { + result.Enabled = override.Enabled + } + + // Labels: override if non-nil (empty slice means "explicitly no labels") + if override.Labels != nil { + result.Labels = override.Labels + } + + // IncludedServers: override if non-nil (empty slice means "no inclusions") + if override.IncludedServers != nil { + result.IncludedServers = override.IncludedServers + } + + // ExcludedServers: override if non-nil (empty slice means "no exclusions") + if override.ExcludedServers != nil { + result.ExcludedServers = override.ExcludedServers + } + + // Type: override if non-empty + if override.Type != "" { + result.Type = override.Type + } + + // Properties: override if non-nil (complete replacement, not deep merge) + if override.Properties != nil { + result.Properties = override.Properties + } + + // Origin: use override's origin if Properties are overridden (since relative + // paths in Properties should resolve relative to where they were defined). + // Otherwise, keep base origin (where Type and original Properties came from). + if override.Properties != nil && override.Origin != "" { + result.Origin = override.Origin + } + + return result +} + +// applyMcpSourceDefaults applies default values to an McpSource for fields that are not set. +func applyMcpSourceDefaults(source mcp.McpSource) mcp.McpSource { + // Default Enabled to true if not set + if source.Enabled == nil { + enabled := true + source.Enabled = &enabled + } + + // Default Labels to empty slice if not set + if source.Labels == nil { + source.Labels = []string{} + } + + return source +} + +// MergeMcpSourcesFromPaths reads MCP source configurations from multiple paths +// and merges sources with the same ID using field-level merging. +// Sources from later paths override fields from earlier paths (priority order). +func MergeMcpSourcesFromPaths(paths []string, readFunc func(path string) ([]mcp.McpSource, error)) (map[string]mcp.McpSource, error) { + // Collect sources by origin in priority order + type originEntry struct { + origin string + sources map[string]mcp.McpSource + } + + entries := make([]originEntry, 0, len(paths)) + + for _, path := range paths { + sources, err := readFunc(path) + if err != nil { + // Return error for invalid config files (caller handles warnings) + continue + } + + sourceMap := make(map[string]mcp.McpSource, len(sources)) + for _, source := range sources { + source.Origin = path + sourceMap[source.Id] = source + } + + entries = append(entries, originEntry{origin: path, sources: sourceMap}) + } + + // Merge sources with field-level priority + result := make(map[string]mcp.McpSource) + + for _, entry := range entries { + for id, source := range entry.sources { + if existing, ok := result[id]; ok { + // Field-level merge: existing is base, source is override + result[id] = mergeMcpSources(existing, source) + } else { + result[id] = source + } + } + } + + // Apply defaults to all merged sources + for id, source := range result { + result[id] = applyMcpSourceDefaults(source) + } + + return result, nil +} diff --git a/catalog/internal/catalog/mcp_source_merge_test.go b/catalog/internal/catalog/mcp_source_merge_test.go new file mode 100644 index 0000000000..364ea785c8 --- /dev/null +++ b/catalog/internal/catalog/mcp_source_merge_test.go @@ -0,0 +1,383 @@ +package catalog + +import ( + "testing" + + "github.com/kubeflow/model-registry/catalog/internal/mcp" + "github.com/stretchr/testify/assert" +) + +func ptrBool(b bool) *bool { + return &b +} + +func TestMergeMcpSources(t *testing.T) { + tests := []struct { + name string + base mcp.McpSource + override mcp.McpSource + expected mcp.McpSource + }{ + { + name: "override takes precedence for all set fields", + base: mcp.McpSource{ + Id: "test-source", + Name: "Base Name", + Type: "yaml", + Enabled: ptrBool(true), + Labels: []string{"base-label"}, + IncludedServers: []string{"base-*"}, + ExcludedServers: []string{"*-deprecated"}, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + override: mcp.McpSource{ + Id: "test-source", + Name: "Override Name", + Type: "remote", + Enabled: ptrBool(false), + Labels: []string{"override-label"}, + IncludedServers: []string{"override-*"}, + ExcludedServers: []string{"*-alpha"}, + Properties: map[string]any{"yamlCatalogPath": "override.yaml"}, + Origin: "/override/path", + }, + expected: mcp.McpSource{ + Id: "test-source", + Name: "Override Name", + Type: "remote", + Enabled: ptrBool(false), + Labels: []string{"override-label"}, + IncludedServers: []string{"override-*"}, + ExcludedServers: []string{"*-alpha"}, + Properties: map[string]any{"yamlCatalogPath": "override.yaml"}, + Origin: "/override/path", // Changed because Properties was overridden + }, + }, + { + name: "empty override preserves base values", + base: mcp.McpSource{ + Id: "test-source", + Name: "Base Name", + Type: "yaml", + Enabled: ptrBool(true), + Labels: []string{"base-label"}, + IncludedServers: []string{"base-*"}, + ExcludedServers: []string{"*-deprecated"}, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + override: mcp.McpSource{ + Id: "test-source", + // All other fields are empty/nil + }, + expected: mcp.McpSource{ + Id: "test-source", + Name: "Base Name", + Type: "yaml", + Enabled: ptrBool(true), + Labels: []string{"base-label"}, + IncludedServers: []string{"base-*"}, + ExcludedServers: []string{"*-deprecated"}, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + }, + { + name: "empty slice in override clears base slices", + base: mcp.McpSource{ + Id: "test-source", + Labels: []string{"label1", "label2"}, + IncludedServers: []string{"include-*"}, + ExcludedServers: []string{"exclude-*"}, + }, + override: mcp.McpSource{ + Id: "test-source", + Labels: []string{}, // Explicitly empty + IncludedServers: []string{}, // Explicitly empty + ExcludedServers: []string{}, // Explicitly empty + }, + expected: mcp.McpSource{ + Id: "test-source", + Labels: []string{}, + IncludedServers: []string{}, + ExcludedServers: []string{}, + }, + }, + { + name: "partial override preserves non-overridden fields", + base: mcp.McpSource{ + Id: "test-source", + Name: "Base Name", + Type: "yaml", + Enabled: ptrBool(true), + Labels: []string{"community"}, + IncludedServers: nil, + ExcludedServers: nil, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + override: mcp.McpSource{ + Id: "test-source", + Labels: []string{"enterprise", "validated"}, // Only override labels + ExcludedServers: []string{"*-alpha"}, // Add exclusions + }, + expected: mcp.McpSource{ + Id: "test-source", + Name: "Base Name", + Type: "yaml", + Enabled: ptrBool(true), + Labels: []string{"enterprise", "validated"}, + IncludedServers: nil, // Preserved from base + ExcludedServers: []string{"*-alpha"}, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + }, + { + name: "origin preserved when properties not overridden", + base: mcp.McpSource{ + Id: "test-source", + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", + }, + override: mcp.McpSource{ + Id: "test-source", + Labels: []string{"new-label"}, + Origin: "/override/path", + // Properties is nil + }, + expected: mcp.McpSource{ + Id: "test-source", + Labels: []string{"new-label"}, + Properties: map[string]any{"yamlCatalogPath": "base.yaml"}, + Origin: "/base/path", // Preserved because Properties wasn't overridden + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mergeMcpSources(tt.base, tt.override) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestApplyMcpSourceDefaults(t *testing.T) { + tests := []struct { + name string + source mcp.McpSource + expected mcp.McpSource + }{ + { + name: "defaults applied when fields are nil", + source: mcp.McpSource{ + Id: "test-source", + Name: "Test", + // Enabled and Labels are nil + }, + expected: mcp.McpSource{ + Id: "test-source", + Name: "Test", + Enabled: ptrBool(true), + Labels: []string{}, + }, + }, + { + name: "no changes when fields already set", + source: mcp.McpSource{ + Id: "test-source", + Enabled: ptrBool(false), + Labels: []string{"custom-label"}, + }, + expected: mcp.McpSource{ + Id: "test-source", + Enabled: ptrBool(false), + Labels: []string{"custom-label"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := applyMcpSourceDefaults(tt.source) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMergeMcpSourcesFromPaths(t *testing.T) { + tests := []struct { + name string + paths []string + sources map[string][]mcp.McpSource // map path -> sources + expected map[string]mcp.McpSource + }{ + { + name: "single path returns sources with defaults", + paths: []string{"/path/one.yaml"}, + sources: map[string][]mcp.McpSource{ + "/path/one.yaml": { + {Id: "source-a", Name: "Source A", Type: "yaml"}, + {Id: "source-b", Name: "Source B", Type: "yaml"}, + }, + }, + expected: map[string]mcp.McpSource{ + "source-a": {Id: "source-a", Name: "Source A", Type: "yaml", Origin: "/path/one.yaml", Enabled: ptrBool(true), Labels: []string{}}, + "source-b": {Id: "source-b", Name: "Source B", Type: "yaml", Origin: "/path/one.yaml", Enabled: ptrBool(true), Labels: []string{}}, + }, + }, + { + name: "later paths override earlier paths", + paths: []string{"/path/base.yaml", "/path/override.yaml"}, + sources: map[string][]mcp.McpSource{ + "/path/base.yaml": { + { + Id: "shared-source", + Name: "Community Source", + Type: "yaml", + Labels: []string{"community"}, + Properties: map[string]any{ + "yamlCatalogPath": "community.yaml", + }, + }, + }, + "/path/override.yaml": { + { + Id: "shared-source", + Labels: []string{"enterprise", "validated"}, + ExcludedServers: []string{"*-alpha", "*-beta"}, + // Type and Properties not set - should be inherited + }, + }, + }, + expected: map[string]mcp.McpSource{ + "shared-source": { + Id: "shared-source", + Name: "Community Source", // Preserved from base + Type: "yaml", // Preserved from base + Labels: []string{"enterprise", "validated"}, + ExcludedServers: []string{"*-alpha", "*-beta"}, + Properties: map[string]any{ + "yamlCatalogPath": "community.yaml", + }, + Origin: "/path/base.yaml", // Preserved (Properties not overridden) + Enabled: ptrBool(true), + }, + }, + }, + { + name: "three-way merge with priority order", + paths: []string{"/default.yaml", "/team.yaml", "/user.yaml"}, + sources: map[string][]mcp.McpSource{ + "/default.yaml": { + { + Id: "k8s-mcp", + Name: "Kubernetes MCP", + Type: "yaml", + Labels: []string{"default"}, + Properties: map[string]any{ + "yamlCatalogPath": "k8s-mcp.yaml", + }, + }, + }, + "/team.yaml": { + { + Id: "k8s-mcp", + Labels: []string{"team", "validated"}, + IncludedServers: []string{"k8s-*"}, + }, + }, + "/user.yaml": { + { + Id: "k8s-mcp", + ExcludedServers: []string{"k8s-deprecated-*"}, + }, + }, + }, + expected: map[string]mcp.McpSource{ + "k8s-mcp": { + Id: "k8s-mcp", + Name: "Kubernetes MCP", + Type: "yaml", + Labels: []string{"team", "validated"}, // From /team.yaml + Properties: map[string]any{ + "yamlCatalogPath": "k8s-mcp.yaml", + }, + IncludedServers: []string{"k8s-*"}, // From /team.yaml + ExcludedServers: []string{"k8s-deprecated-*"}, // From /user.yaml + Origin: "/default.yaml", // Properties from default + Enabled: ptrBool(true), + }, + }, + }, + { + name: "disable source in override", + paths: []string{"/base.yaml", "/override.yaml"}, + sources: map[string][]mcp.McpSource{ + "/base.yaml": { + {Id: "source-a", Name: "Source A", Type: "yaml", Enabled: ptrBool(true)}, + }, + "/override.yaml": { + {Id: "source-a", Enabled: ptrBool(false)}, + }, + }, + expected: map[string]mcp.McpSource{ + "source-a": {Id: "source-a", Name: "Source A", Type: "yaml", Enabled: ptrBool(false), Labels: []string{}, Origin: "/base.yaml"}, + }, + }, + { + name: "multiple independent sources from different paths", + paths: []string{"/path/one.yaml", "/path/two.yaml"}, + sources: map[string][]mcp.McpSource{ + "/path/one.yaml": { + {Id: "source-a", Name: "Source A", Type: "yaml"}, + }, + "/path/two.yaml": { + {Id: "source-b", Name: "Source B", Type: "yaml"}, + }, + }, + expected: map[string]mcp.McpSource{ + "source-a": {Id: "source-a", Name: "Source A", Type: "yaml", Origin: "/path/one.yaml", Enabled: ptrBool(true), Labels: []string{}}, + "source-b": {Id: "source-b", Name: "Source B", Type: "yaml", Origin: "/path/two.yaml", Enabled: ptrBool(true), Labels: []string{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockReadFunc := func(path string) ([]mcp.McpSource, error) { + if sources, ok := tt.sources[path]; ok { + return sources, nil + } + return nil, nil + } + + result, err := MergeMcpSourcesFromPaths(tt.paths, mockReadFunc) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMergeMcpSourcesFromPathsWithMissingFiles(t *testing.T) { + paths := []string{"/missing.yaml", "/valid.yaml"} + sources := map[string][]mcp.McpSource{ + "/valid.yaml": { + {Id: "source-a", Name: "Source A", Type: "yaml"}, + }, + } + + mockReadFunc := func(path string) ([]mcp.McpSource, error) { + if s, ok := sources[path]; ok { + return s, nil + } + return nil, nil // Missing file returns nil + } + + result, err := MergeMcpSourcesFromPaths(paths, mockReadFunc) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "source-a", result["source-a"].Id) +} diff --git a/catalog/internal/catalog/sources.go b/catalog/internal/catalog/sources.go index 50f21a05ba..bad0c5b0ff 100644 --- a/catalog/internal/catalog/sources.go +++ b/catalog/internal/catalog/sources.go @@ -223,7 +223,15 @@ func (sc *SourceCollection) AllSources() map[string]Source { func (sc *SourceCollection) All() map[string]model.CatalogSource { result := map[string]model.CatalogSource{} for id, source := range sc.AllSources() { - result[id] = source.CatalogSource + catalogSource := source.CatalogSource + // Copy DetectedAssetType to the API model's AssetType field + // Default to "models" if not detected (for backwards compatibility) + assetType := model.CATALOGASSETTYPE_MODELS + if source.DetectedAssetType != "" { + assetType = model.CatalogAssetType(source.DetectedAssetType) + } + catalogSource.AssetType = &assetType + result[id] = catalogSource } return result } diff --git a/catalog/internal/catalog/testdata/dev-community-mcp-servers.yaml b/catalog/internal/catalog/testdata/dev-community-mcp-servers.yaml new file mode 100644 index 0000000000..1e46df110c --- /dev/null +++ b/catalog/internal/catalog/testdata/dev-community-mcp-servers.yaml @@ -0,0 +1,616 @@ +source: Community and custom +mcp_servers: + - name: prometheus-mcp + provider: Prometheus Community + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Query Prometheus metrics and alerts directly from your agent. + Provides PromQL query execution, alert retrieval, and metric discovery. + readme: |- + # Prometheus MCP Server + + **MCP Server Summary:** + Access Prometheus metrics and alerting data through the MCP protocol. + + ## Features + - Execute PromQL queries + - Query range data + - Retrieve active alerts + + ## Prerequisites + - Prometheus server access + - Network connectivity to Prometheus API + version: "0.9.2" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/prometheus-community/prometheus-mcp + sourceCode: prometheus-community/prometheus-mcp + publishedDate: "2025-01-10" + tools: + - name: query + description: Execute PromQL queries + accessType: read_only + parameters: + - name: query + type: string + description: PromQL query expression + required: true + - name: query_range + description: Execute PromQL range queries + accessType: read_only + parameters: + - name: query + type: string + description: PromQL query expression + required: true + - name: start + type: string + description: Start timestamp + required: true + - name: end + type: string + description: End timestamp + required: true + - name: get_alerts + description: Retrieve active alerts + accessType: read_only + parameters: [] + artifacts: + - uri: oci://ghcr.io/prometheus-community/prometheus-mcp:0.9.2 + createTimeSinceEpoch: "1736510400000" + lastUpdateTimeSinceEpoch: "1736510400000" + customProperties: + # Tags (MetadataStringValue with empty string_value) + metrics: + metadataType: MetadataStringValue + string_value: "" + monitoring: + metadataType: MetadataStringValue + string_value: "" + alerting: + metadataType: MetadataStringValue + string_value: "" + # Security indicators (MetadataBoolValue) + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736510400000" + lastUpdateTimeSinceEpoch: "1736510400000" + + - name: kubernetes-mcp + provider: CNCF + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Interact with Kubernetes clusters through natural language. + List pods, check deployments, view logs, and manage resources. + readme: |- + # Kubernetes MCP Server + + **MCP Server Summary:** + Comprehensive Kubernetes cluster management through MCP. + + ## Features + - Pod management and listing + - Deployment operations + - Log retrieval + - Scaling operations + + ## Prerequisites + - kubectl configured with cluster access + - Appropriate RBAC permissions + version: "1.2.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/cncf/kubernetes-mcp + sourceCode: cncf/kubernetes-mcp + publishedDate: "2025-01-12" + tools: + - name: get_pods + description: List pods in a namespace + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: get_deployments + description: List deployments + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: get_logs + description: Retrieve pod logs + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: pod_name + type: string + description: Name of the pod + required: true + - name: scale_deployment + description: Scale a deployment + accessType: read_write + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: deployment_name + type: string + description: Name of the deployment + required: true + - name: replicas + type: number + description: Desired number of replicas + required: true + artifacts: + - uri: oci://ghcr.io/cncf/kubernetes-mcp:1.2.0 + createTimeSinceEpoch: "1736683200000" + lastUpdateTimeSinceEpoch: "1736683200000" + customProperties: + kubernetes: + metadataType: MetadataStringValue + string_value: "" + containers: + metadataType: MetadataStringValue + string_value: "" + orchestration: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736683200000" + lastUpdateTimeSinceEpoch: "1736683200000" + + - name: openshift-mcp + provider: Red Hat + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Red Hat OpenShift integration for container platform management. + Deploy applications, manage routes, and monitor cluster health. + readme: |- + # OpenShift MCP Server + + **MCP Server Summary:** + Red Hat OpenShift container platform integration. + + ## Features + - Project listing + - Route management + - Build operations + + ## Prerequisites + - OpenShift CLI (oc) configured + - Cluster admin or developer access + version: "1.0.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/redhat/openshift-mcp + sourceCode: redhat/openshift-mcp + publishedDate: "2025-01-13" + tools: + - name: get_projects + description: List OpenShift projects + accessType: read_only + parameters: [] + - name: get_routes + description: List routes in a project + accessType: read_only + parameters: + - name: project + type: string + description: Project name + required: true + - name: get_builds + description: List builds and their status + accessType: read_only + parameters: + - name: project + type: string + description: Project name + required: true + - name: start_build + description: Trigger a new build + accessType: read_write + parameters: + - name: project + type: string + description: Project name + required: true + - name: build_config + type: string + description: Build configuration name + required: true + artifacts: + - uri: oci://registry.redhat.io/openshift/openshift-mcp:1.0.0 + createTimeSinceEpoch: "1736769600000" + lastUpdateTimeSinceEpoch: "1736769600000" + customProperties: + openshift: + metadataType: MetadataStringValue + string_value: "" + containers: + metadataType: MetadataStringValue + string_value: "" + kubernetes: + metadataType: MetadataStringValue + string_value: "" + enterprise: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736769600000" + lastUpdateTimeSinceEpoch: "1736769600000" + + - name: elasticsearch-mcp + provider: Elastic + license: elastic-2.0 + license_link: https://www.elastic.co/licensing/elastic-license + description: >- + Query and analyze data in Elasticsearch clusters. + Execute searches, aggregations, and manage indices. + readme: |- + # Elasticsearch MCP Server + + **MCP Server Summary:** + Elasticsearch data query and analysis. + + ## Features + - Search queries + - Aggregations + - Index management + + ## Prerequisites + - Elasticsearch cluster access + - API credentials (if authentication enabled) + version: "0.8.0" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/elastic/elasticsearch-mcp + sourceCode: elastic/elasticsearch-mcp + publishedDate: "2024-12-20" + tools: + - name: search + description: Execute a search query + accessType: read_only + parameters: + - name: index + type: string + description: Index name or pattern + required: true + - name: aggregate + description: Execute an aggregation query + accessType: read_only + parameters: + - name: index + type: string + description: Index name or pattern + required: true + - name: get_indices + description: List available indices + accessType: read_only + parameters: [] + artifacts: + - uri: oci://docker.elastic.co/elasticsearch/elasticsearch-mcp:0.8.0 + createTimeSinceEpoch: "1734696000000" + lastUpdateTimeSinceEpoch: "1734696000000" + customProperties: + search: + metadataType: MetadataStringValue + string_value: "" + analytics: + metadataType: MetadataStringValue + string_value: "" + logging: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: false + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1734696000000" + lastUpdateTimeSinceEpoch: "1734696000000" + + # Remote MCP Servers - hosted externally, accessed via network endpoints + # Note: Remote servers do not have artifacts as they are hosted externally + - name: openai-assistants-mcp + provider: OpenAI + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Access OpenAI Assistants API for advanced AI conversations and tool use. + This is a remotely hosted MCP server provided by OpenAI's cloud infrastructure. + readme: |- + # OpenAI Assistants MCP Server + + **MCP Server Summary:** + Remotely hosted MCP server for OpenAI Assistants integration. + + ## Features + - Create and manage AI assistants + - Execute conversations with tool use + - Retrieve assistant responses + + ## Note + This is a remote MCP server hosted by OpenAI. No local deployment required. + deploymentMode: remote + endpoints: + http: https://api.openai.com/v1/mcp + sse: https://api.openai.com/v1/mcp/stream + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://platform.openai.com/docs/assistants + repositoryUrl: https://github.com/openai/openai-mcp + sourceCode: openai/openai-mcp + publishedDate: "2025-01-20" + tools: + - name: create_assistant + description: Create a new AI assistant + accessType: read_write + parameters: + - name: name + type: string + description: Assistant name + required: true + - name: instructions + type: string + description: System instructions for the assistant + required: true + - name: chat + description: Send a message to an assistant + accessType: read_write + parameters: + - name: assistant_id + type: string + description: ID of the assistant + required: true + - name: message + type: string + description: User message + required: true + - name: list_assistants + description: List available assistants + accessType: read_only + parameters: [] + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + assistants: + metadataType: MetadataStringValue + string_value: "" + cloud: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1737388800000" + lastUpdateTimeSinceEpoch: "1737388800000" + + - name: anthropic-claude-mcp + provider: Anthropic + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Access Anthropic's Claude models through a remotely hosted MCP server. + Enables tool use, multi-turn conversations, and computer use capabilities. + readme: |- + # Anthropic Claude MCP Server + + **MCP Server Summary:** + Remote MCP server for Claude AI integration. + + ## Features + - Multi-turn conversations with Claude + - Tool use and function calling + - Computer use capabilities (beta) + + ## Note + This server is hosted by Anthropic. API key required for access. + deploymentMode: remote + endpoints: + sse: https://api.anthropic.com/v1/mcp/events + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.anthropic.com/mcp + repositoryUrl: https://github.com/anthropic/claude-mcp + sourceCode: anthropic/claude-mcp + publishedDate: "2025-01-18" + tools: + - name: chat + description: Send a message to Claude + accessType: read_only + parameters: + - name: message + type: string + description: User message + required: true + - name: model + type: string + description: Claude model to use (e.g., claude-3-opus) + required: false + - name: use_tool + description: Have Claude use a tool + accessType: read_write + parameters: + - name: tool_name + type: string + description: Name of the tool + required: true + - name: parameters + type: object + description: Tool parameters + required: true + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + claude: + metadataType: MetadataStringValue + string_value: "" + anthropic: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1737216000000" + lastUpdateTimeSinceEpoch: "1737216000000" + + - name: google-gemini-mcp + provider: Google + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Google Gemini AI integration through a managed remote MCP server. + Access multimodal capabilities, code generation, and reasoning. + readme: |- + # Google Gemini MCP Server + + **MCP Server Summary:** + Remote MCP server for Google Gemini AI. + + ## Features + - Multimodal input (text, images, video) + - Code generation and execution + - Advanced reasoning capabilities + + ## Note + Hosted on Google Cloud. API key required. + deploymentMode: remote + endpoints: + http: https://generativelanguage.googleapis.com/v1/mcp + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://ai.google.dev/docs + repositoryUrl: https://github.com/google/gemini-mcp + sourceCode: google/gemini-mcp + publishedDate: "2025-01-15" + tools: + - name: generate + description: Generate content with Gemini + accessType: read_only + parameters: + - name: prompt + type: string + description: Text prompt + required: true + - name: model + type: string + description: Model version (gemini-pro, gemini-ultra) + required: false + - name: analyze_image + description: Analyze an image with Gemini Vision + accessType: read_only + parameters: + - name: image_url + type: string + description: URL of the image to analyze + required: true + - name: prompt + type: string + description: Question about the image + required: true + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + gemini: + metadataType: MetadataStringValue + string_value: "" + google: + metadataType: MetadataStringValue + string_value: "" + multimodal: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736956800000" + lastUpdateTimeSinceEpoch: "1736956800000" diff --git a/catalog/internal/catalog/testdata/dev-mcp-catalog-sources.yaml b/catalog/internal/catalog/testdata/dev-mcp-catalog-sources.yaml new file mode 100644 index 0000000000..8a7d003cba --- /dev/null +++ b/catalog/internal/catalog/testdata/dev-mcp-catalog-sources.yaml @@ -0,0 +1,22 @@ +catalogs: + - name: "Organization MCP Servers" + id: organization_mcp_servers + type: yaml + enabled: true + properties: + yamlCatalogPath: dev-organization-mcp-servers.yaml + labels: + - Organization MCP + # Example filter: only include GitHub and Slack MCP servers, exclude deprecated ones + includedServers: + - "github-*" + - "slack-*" + excludedServers: + - "*-deprecated" + - name: "Community and Custom MCP Servers" + id: community_mcp_servers + type: yaml + enabled: true + properties: + yamlCatalogPath: dev-community-mcp-servers.yaml + # No filters - include all servers from this source diff --git a/catalog/internal/catalog/testdata/dev-mcp-override-sources.yaml b/catalog/internal/catalog/testdata/dev-mcp-override-sources.yaml new file mode 100644 index 0000000000..88966719d3 --- /dev/null +++ b/catalog/internal/catalog/testdata/dev-mcp-override-sources.yaml @@ -0,0 +1,8 @@ +catalogs: + - id: organization_mcp_servers + labels: + - Validated + - Enterprise + excludedServers: + - "*-alpha" + - "*-beta" diff --git a/catalog/internal/catalog/testdata/dev-organization-mcp-servers.yaml b/catalog/internal/catalog/testdata/dev-organization-mcp-servers.yaml new file mode 100644 index 0000000000..d67de6c75f --- /dev/null +++ b/catalog/internal/catalog/testdata/dev-organization-mcp-servers.yaml @@ -0,0 +1,474 @@ +source: Organization MCP +mcp_servers: + - name: dynatrace-mcp + provider: Dynatrace + license: apache-2.0 + license_link: https://github.com/dynatrace-oss/dynatrace-mcp-server/blob/main/LICENSE + description: >- + Official Dynatrace-OSS project exposing DQL queries, problem feeds, and vulnerability data. + Gives agents real-time service health, letting them recommend rollbacks or capacity fixes inside OpenShift. + readme: |- + # Dynatrace MCP Server + + **MCP Server Summary:** + The Dynatrace MCP Server provides AI agents with access to observability data, enabling intelligent monitoring and analysis. + + ## Features + - Execute DQL queries for real-time observability + - Access problem feeds and incident information + - Retrieve vulnerability data from monitored services + + ## Prerequisites + - Dynatrace API token with appropriate permissions + - Dynatrace environment URL + + ## Installation + ```bash + npm install @dynatrace-oss/dynatrace-mcp-server + ``` + + ## Configuration + Set the following environment variables: + - `DYNATRACE_API_TOKEN`: Your Dynatrace API token + - `DYNATRACE_ENV_URL`: Your Dynatrace environment URL + version: "1.0.1" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.dynatrace.com/mcp + repositoryUrl: https://github.com/dynatrace-oss/dynatrace-mcp-server + sourceCode: dynatrace-oss/dynatrace-mcp-server + publishedDate: "2025-01-15" + tools: + - name: execute_dql + description: Execute Dynatrace Query Language (DQL) queries + accessType: read_only + parameters: + - name: query + type: string + description: DQL query to execute + required: true + - name: time_range + type: string + description: Time range for query (e.g., -1h, -24h) + required: false + - name: get_problems + description: Retrieve active problems and incidents + accessType: read_only + parameters: + - name: status + type: string + description: "Filter by status: OPEN, RESOLVED, or ALL" + required: false + - name: get_vulnerabilities + description: Query security vulnerabilities in monitored services + accessType: read_only + parameters: + - name: severity + type: string + description: "Minimum severity: CRITICAL, HIGH, MEDIUM, LOW" + required: false + artifacts: + - uri: oci://ghcr.io/dynatrace-oss/dynatrace-mcp-server:1.0.1 + createTimeSinceEpoch: "1736942400000" + lastUpdateTimeSinceEpoch: "1736942400000" + customProperties: + observability: + metadataType: MetadataStringValue + string_value: "" + monitoring: + metadataType: MetadataStringValue + string_value: "" + apm: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736942400000" + lastUpdateTimeSinceEpoch: "1736942400000" + + - name: github-mcp + provider: GitHub + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Access GitHub repositories, issues, pull requests, and actions. + Search code, create issues, and manage workflows. + readme: |- + # GitHub MCP Server + + **MCP Server Summary:** + Full GitHub integration for AI agents. + + ## Features + - Code search across repositories + - Issue management + - Pull request operations + + ## Prerequisites + - GitHub Personal Access Token + - Appropriate repository permissions + version: "2.1.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.github.com/mcp + repositoryUrl: https://github.com/github/github-mcp + sourceCode: github/github-mcp + publishedDate: "2025-01-14" + tools: + - name: search_code + description: Search for code across repositories + accessType: read_only + parameters: + - name: query + type: string + description: Search query + required: true + - name: get_issues + description: List issues for a repository + accessType: read_only + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + - name: create_issue + description: Create a new issue + accessType: read_write + revoked: true + revokedReason: "Security vulnerability CVE-2025-1234 - write operations temporarily disabled pending patch" + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + - name: title + type: string + description: Issue title + required: true + - name: get_pull_requests + description: List pull requests + accessType: read_only + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + artifacts: + - uri: oci://ghcr.io/github/github-mcp:2.1.0 + createTimeSinceEpoch: "1736856000000" + lastUpdateTimeSinceEpoch: "1736856000000" + customProperties: + git: + metadataType: MetadataStringValue + string_value: "" + version-control: + metadataType: MetadataStringValue + string_value: "" + ci-cd: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736856000000" + lastUpdateTimeSinceEpoch: "1736856000000" + + - name: slack-mcp + provider: Slack Technologies + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Send messages, manage channels, and integrate with Slack workspaces. + Perfect for automated notifications and team communication. + readme: |- + # Slack MCP Server + + **MCP Server Summary:** + Slack workspace integration for AI agents. + + ## Features + - Send messages to channels + - List channels + - Retrieve message history + + ## Prerequisites + - Slack Bot Token + - Appropriate workspace permissions + version: "1.5.0" + transports: + - sse + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/slack/slack-mcp + sourceCode: slack/slack-mcp + publishedDate: "2025-01-08" + tools: + - name: send_message + description: Send a message to a channel + accessType: read_write + revoked: true + revokedReason: "Rate limiting issues detected - tool temporarily disabled while investigating" + parameters: + - name: channel + type: string + description: Channel ID or name + required: true + - name: text + type: string + description: Message text + required: true + - name: list_channels + description: List available channels + accessType: read_only + parameters: [] + - name: get_channel_history + description: Get message history for a channel + accessType: read_only + parameters: + - name: channel + type: string + description: Channel ID + required: true + artifacts: + - uri: oci://ghcr.io/slack/slack-mcp:1.5.0 + createTimeSinceEpoch: "1736337600000" + lastUpdateTimeSinceEpoch: "1736337600000" + customProperties: + messaging: + metadataType: MetadataStringValue + string_value: "" + collaboration: + metadataType: MetadataStringValue + string_value: "" + notifications: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736337600000" + lastUpdateTimeSinceEpoch: "1736337600000" + + - name: jira-mcp + provider: Atlassian + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Manage Jira issues, projects, and workflows. + Create, update, and query issues using natural language. + readme: |- + # Jira MCP Server + + **MCP Server Summary:** + Jira project management integration. + + ## Features + - JQL search + - Issue creation and updates + - Workflow management + + ## Prerequisites + - Jira API token + - Site URL configuration + version: "1.3.2" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://developer.atlassian.com/mcp + repositoryUrl: https://github.com/atlassian/jira-mcp + sourceCode: atlassian/jira-mcp + publishedDate: "2025-01-11" + tools: + - name: search_issues + description: Search issues using JQL + accessType: read_only + parameters: + - name: jql + type: string + description: JQL query + required: true + - name: get_issue + description: Get issue details + accessType: read_only + parameters: + - name: issue_key + type: string + description: Issue key (e.g., PROJ-123) + required: true + - name: create_issue + description: Create a new issue + accessType: read_write + parameters: + - name: project + type: string + description: Project key + required: true + - name: summary + type: string + description: Issue summary + required: true + - name: issue_type + type: string + description: Issue type + required: true + - name: update_issue + description: Update an existing issue + accessType: read_write + parameters: + - name: issue_key + type: string + description: Issue key + required: true + artifacts: + - uri: oci://ghcr.io/atlassian/jira-mcp:1.3.2 + createTimeSinceEpoch: "1736596800000" + lastUpdateTimeSinceEpoch: "1736596800000" + customProperties: + project-management: + metadataType: MetadataStringValue + string_value: "" + issue-tracking: + metadataType: MetadataStringValue + string_value: "" + agile: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736596800000" + lastUpdateTimeSinceEpoch: "1736596800000" + + - name: aws-mcp + provider: Amazon Web Services + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Interact with Amazon Web Services resources. + Manage EC2 instances, S3 buckets, Lambda functions, and more. + readme: |- + # AWS MCP Server + + **MCP Server Summary:** + Amazon Web Services resource management. + + ## Features + - EC2 instance management + - S3 bucket operations + - Lambda invocation + - CloudWatch monitoring + + ## Prerequisites + - AWS credentials configured + - Appropriate IAM permissions + version: "2.0.1" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.aws.amazon.com/mcp + repositoryUrl: https://github.com/aws/aws-mcp + sourceCode: aws/aws-mcp + publishedDate: "2025-01-09" + tools: + - name: list_ec2_instances + description: List EC2 instances + accessType: read_only + parameters: + - name: region + type: string + description: AWS region + required: true + - name: list_s3_buckets + description: List S3 buckets + accessType: read_only + parameters: [] + - name: invoke_lambda + description: Invoke a Lambda function + accessType: read_write + parameters: + - name: function_name + type: string + description: Lambda function name or ARN + required: true + - name: describe_cloudwatch_alarms + description: List CloudWatch alarms + accessType: read_only + parameters: [] + artifacts: + - uri: oci://public.ecr.aws/aws/aws-mcp:2.0.1 + createTimeSinceEpoch: "1736424000000" + lastUpdateTimeSinceEpoch: "1736424000000" + customProperties: + cloud: + metadataType: MetadataStringValue + string_value: "" + aws: + metadataType: MetadataStringValue + string_value: "" + infrastructure: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736424000000" + lastUpdateTimeSinceEpoch: "1736424000000" diff --git a/catalog/internal/catalog/yaml_catalog.go b/catalog/internal/catalog/yaml_catalog.go index 1116af50f3..ec65c31d0d 100644 --- a/catalog/internal/catalog/yaml_catalog.go +++ b/catalog/internal/catalog/yaml_catalog.go @@ -384,6 +384,24 @@ func (p *yamlModelProvider) emit(ctx context.Context, catalog *yamlCatalog, out } func newYamlModelProvider(ctx context.Context, source *Source, reldir string) (<-chan ModelProviderRecord, error) { + // First, detect the asset type from the YAML content + assetType, err := DetectYamlAssetType(source, reldir) + if err != nil { + return nil, err + } + + // Only process this source if it contains models + if assetType != AssetTypeModels { + glog.V(2).Infof("Skipping source %s in model provider: detected asset type is %s", source.Id, assetType) + // Return an empty channel that closes immediately + ch := make(chan ModelProviderRecord) + close(ch) + return ch, nil + } + + // Store the detected asset type for API responses + source.DetectedAssetType = assetType + p := &yamlModelProvider{} path, exists := source.Properties[yamlCatalogPathKey].(string) diff --git a/catalog/internal/catalog/yaml_common.go b/catalog/internal/catalog/yaml_common.go new file mode 100644 index 0000000000..83a0c3057a --- /dev/null +++ b/catalog/internal/catalog/yaml_common.go @@ -0,0 +1,12 @@ +package catalog + +import ( + "github.com/kubeflow/model-registry/catalog/internal/common" +) + +// DetectYamlAssetType reads a YAML file and detects what type of assets it contains. +// It returns an error if the file has multiple asset types or no recognized asset types. +// This is a convenience wrapper around common.DetectYamlAssetType for Source types. +func DetectYamlAssetType(source *Source, reldir string) (AssetType, error) { + return common.DetectYamlAssetType(source, reldir) +} diff --git a/catalog/internal/common/asset_types.go b/catalog/internal/common/asset_types.go new file mode 100644 index 0000000000..021371fbe5 --- /dev/null +++ b/catalog/internal/common/asset_types.go @@ -0,0 +1,88 @@ +// Package common provides shared types and utilities used across catalog packages. +// This package exists to avoid circular imports between catalog and mcp packages. +package common + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const ( + // YamlCatalogPathKey is the property key for the YAML catalog file path + YamlCatalogPathKey = "yamlCatalogPath" +) + +// AssetType represents the type of assets contained in a catalog source +type AssetType string + +const ( + // AssetTypeModels indicates the source contains AI/ML models + AssetTypeModels AssetType = "models" + // AssetTypeMcpServers indicates the source contains MCP servers + AssetTypeMcpServers AssetType = "mcp_servers" +) + +// SourceProperties is an interface for accessing source configuration properties. +// It is used to avoid circular imports between catalog and mcp packages. +type SourceProperties interface { + GetId() string + GetProperties() map[string]any +} + +// yamlAssetDetector represents a minimal YAML structure for detecting asset types +// Note: Uses json tags because k8s.io/apimachinery/pkg/util/yaml converts YAML to JSON first +type yamlAssetDetector struct { + Models []any `json:"models" yaml:"models"` + McpServers []any `json:"mcp_servers" yaml:"mcp_servers"` +} + +// DetectYamlAssetType reads a YAML file and detects what type of assets it contains. +// It returns an error if the file has multiple asset types. +func DetectYamlAssetType(props SourceProperties, reldir string) (AssetType, error) { + path, exists := props.GetProperties()[YamlCatalogPathKey].(string) + if !exists || path == "" { + return "", fmt.Errorf("missing %s string property", YamlCatalogPathKey) + } + + var fullPath string + if filepath.IsAbs(path) { + fullPath = path + } else { + fullPath = filepath.Join(reldir, path) + } + + buf, err := os.ReadFile(fullPath) + if err != nil { + return "", fmt.Errorf("failed to read YAML file %s: %v", fullPath, err) + } + + var detector yamlAssetDetector + if err = yaml.Unmarshal(buf, &detector); err != nil { + return "", fmt.Errorf("failed to parse YAML file %s: %v", fullPath, err) + } + + hasModels := len(detector.Models) > 0 + hasMcpServers := len(detector.McpServers) > 0 + + glog.V(2).Infof("YAML asset detection for %s: models=%v, mcp_servers=%v", fullPath, hasModels, hasMcpServers) + + if hasModels && hasMcpServers { + return "", fmt.Errorf("YAML file %s contains multiple asset types (models and mcp_servers); each file should contain only one asset type", fullPath) + } + + if hasModels { + return AssetTypeModels, nil + } + + if hasMcpServers { + return AssetTypeMcpServers, nil + } + + // Default to models for backwards compatibility if neither key is present + glog.V(2).Infof("YAML file %s has no recognized asset type keys, defaulting to models", fullPath) + return AssetTypeModels, nil +} diff --git a/catalog/internal/db/filter/entity_mappings.go b/catalog/internal/db/filter/entity_mappings.go index c0430f1106..22799ac7ed 100644 --- a/catalog/internal/db/filter/entity_mappings.go +++ b/catalog/internal/db/filter/entity_mappings.go @@ -12,6 +12,8 @@ type CatalogRestEntityType string const ( RestEntityCatalogModel CatalogRestEntityType = "CatalogModel" RestEntityCatalogArtifact CatalogRestEntityType = "CatalogArtifact" + RestEntityMcpServer CatalogRestEntityType = "McpServer" + RestEntityMcpServerTool CatalogRestEntityType = "McpServerTool" ) // catalogEntityMappings implements EntityMappingFunctions for the catalog package @@ -67,6 +69,20 @@ func (c *catalogEntityMappings) GetPropertyDefinitionForRestEntity(restEntityTyp } } + if restEntityType == filter.RestEntityType(RestEntityMcpServer) { + if _, isWellKnown := mcpServerProperties[propertyName]; isWellKnown { + // Use the well-known property definition + return mcpServerProperties[propertyName] + } + } + + if restEntityType == filter.RestEntityType(RestEntityMcpServerTool) { + if _, isWellKnown := mcpServerToolProperties[propertyName]; isWellKnown { + // Use the well-known property definition + return mcpServerToolProperties[propertyName] + } + } + // Not a well-known property for this entity type, treat as custom return filter.PropertyDefinition{ Location: filter.Custom, @@ -119,3 +135,51 @@ var catalogArtifactProperties = map[string]filter.PropertyDefinition{ // Artifact type (stored in type_id but we can filter by string representation) "artifactType": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "artifactType"}, } + +// mcpServerProperties defines the allowed properties for McpServer entities. +// This follows the same pattern as catalogModelProperties - only properties that are: +// 1. Entity table columns (required for core identity) +// 2. Key filterable dimensions that need explicit type handling (arrays, bools) +// 3. Common properties shared with CatalogModel for consistency +// All other properties can be queried via custom property fallback. +var mcpServerProperties = map[string]filter.PropertyDefinition{ + // Common Context properties (Entity Table - required) + "id": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "id"}, + "name": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "name"}, + "externalId": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "external_id"}, + "createTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "create_time_since_epoch"}, + "lastUpdateTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "last_update_time_since_epoch"}, + + // Core properties matching CatalogModel pattern + "source_id": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "source_id"}, + "description": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "description"}, + "provider": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "provider"}, + "license": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "license"}, + "license_link": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "license_link"}, + "logo": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "logo"}, + "readme": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "readme"}, + + // MCP-specific filterable dimensions (need explicit type for arrays) + "tags": {Location: filter.PropertyTable, ValueType: filter.ArrayValueType, Column: "tags"}, + "transports": {Location: filter.PropertyTable, ValueType: filter.ArrayValueType, Column: "transports"}, + "deploymentMode": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "deploymentMode"}, + + // Security indicators (need explicit type for booleans) + "verifiedSource": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "verifiedSource"}, + "secureEndpoint": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "secureEndpoint"}, + "sast": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "sast"}, + "readOnlyTools": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "readOnlyTools"}, +} + +// mcpServerToolProperties defines the allowed properties for McpServerTool entities +var mcpServerToolProperties = map[string]filter.PropertyDefinition{ + // Common Artifact properties + "id": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "id"}, + "name": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "name"}, + "createTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "create_time_since_epoch"}, + "lastUpdateTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "last_update_time_since_epoch"}, + + // McpServerTool-specific properties stored in ArtifactProperty table + "description": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "description"}, + "accessType": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "accessType"}, +} diff --git a/catalog/internal/db/models/mcp_server.go b/catalog/internal/db/models/mcp_server.go new file mode 100644 index 0000000000..ec9a54930d --- /dev/null +++ b/catalog/internal/db/models/mcp_server.go @@ -0,0 +1,58 @@ +package models + +import ( + catalogfilter "github.com/kubeflow/model-registry/catalog/internal/db/filter" + "github.com/kubeflow/model-registry/internal/db/filter" + "github.com/kubeflow/model-registry/internal/db/models" +) + +// McpServerListOptions holds the options for listing MCP servers. +type McpServerListOptions struct { + models.Pagination + Name *string + SourceIDs *[]string + Query *string + TextSearch *string + FilterQuery *string + NamedQuery *string +} + +// GetRestEntityType implements the FilterApplier interface. +func (c *McpServerListOptions) GetRestEntityType() filter.RestEntityType { + return filter.RestEntityType(catalogfilter.RestEntityMcpServer) +} + +// GetFilterQuery returns the filter query string for advanced filtering. +func (c *McpServerListOptions) GetFilterQuery() string { + if c.FilterQuery == nil { + return "" + } + return *c.FilterQuery +} + +// McpServerAttributes holds the attributes for an MCP server record. +type McpServerAttributes struct { + Name *string + ExternalID *string + CreateTimeSinceEpoch *int64 + LastUpdateTimeSinceEpoch *int64 +} + +// McpServer represents an MCP server stored in the database. +type McpServer interface { + models.Entity[McpServerAttributes] +} + +// McpServerImpl is the concrete implementation of McpServer. +type McpServerImpl = models.BaseEntity[McpServerAttributes] + +// McpServerRepository defines the interface for MCP server persistence. +type McpServerRepository interface { + GetByID(id int32) (McpServer, error) + GetByName(name string) (McpServer, error) + List(listOptions McpServerListOptions) (*models.ListWrapper[McpServer], error) + Save(server McpServer) (McpServer, error) + DeleteBySource(sourceID string) error + DeleteByID(id int32) error + GetDistinctSourceIDs() ([]string, error) +} diff --git a/catalog/internal/db/models/mcp_server_tool.go b/catalog/internal/db/models/mcp_server_tool.go new file mode 100644 index 0000000000..fbaea42e8b --- /dev/null +++ b/catalog/internal/db/models/mcp_server_tool.go @@ -0,0 +1,29 @@ +package models + +import ( + "github.com/kubeflow/model-registry/internal/db/models" +) + +// McpServerToolAttributes holds the attributes for an MCP server tool record. +type McpServerToolAttributes struct { + Name *string + CreateTimeSinceEpoch *int64 + LastUpdateTimeSinceEpoch *int64 +} + +// McpServerTool represents an MCP server tool stored in the database. +type McpServerTool interface { + models.Entity[McpServerToolAttributes] +} + +// McpServerToolImpl is the concrete implementation of McpServerTool. +type McpServerToolImpl = models.BaseEntity[McpServerToolAttributes] + +// McpServerToolRepository defines the interface for MCP server tool persistence. +type McpServerToolRepository interface { + GetByID(id int32) (McpServerTool, error) + List(parentID int32) ([]McpServerTool, error) + Save(tool McpServerTool, parentID *int32) (McpServerTool, error) + DeleteByParentID(parentID int32) error + DeleteByID(id int32) error +} diff --git a/catalog/internal/db/service/mcp_server.go b/catalog/internal/db/service/mcp_server.go new file mode 100644 index 0000000000..e80f043e80 --- /dev/null +++ b/catalog/internal/db/service/mcp_server.go @@ -0,0 +1,383 @@ +package service + +import ( + "errors" + "fmt" + "strings" + + "github.com/golang/glog" + "github.com/kubeflow/model-registry/catalog/internal/db/filter" + "github.com/kubeflow/model-registry/catalog/internal/db/models" + "github.com/kubeflow/model-registry/internal/db/dbutil" + dbmodels "github.com/kubeflow/model-registry/internal/db/models" + "github.com/kubeflow/model-registry/internal/db/schema" + "github.com/kubeflow/model-registry/internal/db/scopes" + "github.com/kubeflow/model-registry/internal/db/service" + "github.com/kubeflow/model-registry/internal/db/utils" + "gorm.io/gorm" +) + +var ErrMcpServerNotFound = errors.New("MCP server by id not found") + +// McpServerRepositoryImpl implements McpServerRepository using GORM. +type McpServerRepositoryImpl struct { + *service.GenericRepository[models.McpServer, schema.Context, schema.ContextProperty, *models.McpServerListOptions] +} + +// NewMcpServerRepository creates a new McpServerRepository. +func NewMcpServerRepository(db *gorm.DB, typeID int32) models.McpServerRepository { + r := &McpServerRepositoryImpl{} + + r.GenericRepository = service.NewGenericRepository(service.GenericRepositoryConfig[models.McpServer, schema.Context, schema.ContextProperty, *models.McpServerListOptions]{ + DB: db, + TypeID: typeID, + EntityToSchema: mapMcpServerToContext, + SchemaToEntity: mapDataLayerToMcpServer, + EntityToProperties: mapMcpServerToContextProperties, + NotFoundError: ErrMcpServerNotFound, + EntityName: "MCP server", + PropertyFieldName: "context_id", + ApplyListFilters: applyMcpServerListFilters, + CreatePaginationToken: r.createPaginationToken, + ApplyCustomOrdering: r.applyCustomOrdering, + IsNewEntity: func(entity models.McpServer) bool { return entity.GetID() == nil }, + HasCustomProperties: func(entity models.McpServer) bool { return entity.GetCustomProperties() != nil }, + EntityMappingFuncs: filter.NewCatalogEntityMappings(), + PreserveHistoricalTimes: true, // Preserve timestamps from YAML source data + }) + + return r +} + +// Save creates or updates an MCP server. +func (r *McpServerRepositoryImpl) Save(server models.McpServer) (models.McpServer, error) { + config := r.GetConfig() + if server.GetTypeID() == nil { + if config.TypeID > 0 { + server.SetTypeID(config.TypeID) + } + } + + attr := server.GetAttributes() + if server.GetID() == nil && attr != nil && attr.Name != nil { + existing, err := r.lookupServerByName(*attr.Name) + if err != nil { + if !errors.Is(err, ErrMcpServerNotFound) { + return nil, fmt.Errorf("error finding existing MCP server named %s: %w", *attr.Name, err) + } + } else { + server.SetID(existing.ID) + } + } + + return r.GenericRepository.Save(server, nil) +} + +// List returns a paginated list of MCP servers. +func (r *McpServerRepositoryImpl) List(listOptions models.McpServerListOptions) (*dbmodels.ListWrapper[models.McpServer], error) { + return r.GenericRepository.List(&listOptions) +} + +// GetByName retrieves an MCP server by its name. +func (r *McpServerRepositoryImpl) GetByName(name string) (models.McpServer, error) { + var zeroEntity models.McpServer + entity, err := r.lookupServerByName(name) + if err != nil { + return zeroEntity, err + } + + config := r.GetConfig() + + // Query properties + var properties []schema.ContextProperty + if err := config.DB.Where(config.PropertyFieldName+" = ?", entity.ID).Find(&properties).Error; err != nil { + err = dbutil.SanitizeDatabaseError(err) + return zeroEntity, fmt.Errorf("error getting properties by %s id: %w", config.EntityName, err) + } + + // Map to domain model + return config.SchemaToEntity(*entity, properties), nil +} + +// lookupServerByName finds an MCP server by name. +func (r *McpServerRepositoryImpl) lookupServerByName(name string) (*schema.Context, error) { + var entity schema.Context + + config := r.GetConfig() + + if err := config.DB.Where("name = ? AND type_id = ?", name, config.TypeID).First(&entity).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: %v", config.NotFoundError, err) + } + err = dbutil.SanitizeDatabaseError(err) + return nil, fmt.Errorf("error getting %s by name: %w", config.EntityName, err) + } + + return &entity, nil +} + +// DeleteBySource deletes all MCP servers from a given source. +func (r *McpServerRepositoryImpl) DeleteBySource(sourceID string) error { + config := r.GetConfig() + + // Delete all Context records where there's a ContextProperty with name='source_id' and string_value=sourceID + query := `DELETE FROM "Context" WHERE id IN ( + SELECT "Context".id + FROM "Context" + INNER JOIN "ContextProperty" ON "Context".id="ContextProperty".context_id + AND "ContextProperty".name='source_id' + WHERE "ContextProperty".string_value=? + AND "Context".type_id=? + )` + + return config.DB.Exec(query, sourceID, config.TypeID).Error +} + +// DeleteByID deletes an MCP server by its ID. +func (r *McpServerRepositoryImpl) DeleteByID(id int32) error { + config := r.GetConfig() + + result := config.DB.Where("id = ? AND type_id = ?", id, config.TypeID).Delete(&schema.Context{}) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return fmt.Errorf("%w: id %d", config.NotFoundError, id) + } + + return nil +} + +// GetDistinctSourceIDs retrieves all unique source_id values from MCP servers. +func (r *McpServerRepositoryImpl) GetDistinctSourceIDs() ([]string, error) { + config := r.GetConfig() + + var sourceIDs []string + + query := `SELECT DISTINCT cp.string_value FROM "ContextProperty" cp + INNER JOIN "Context" c ON cp.context_id = c.id + WHERE cp.name='source_id' AND c.type_id=?` + + rows, err := config.DB.Raw(query, config.TypeID).Rows() + if err != nil { + err = dbutil.SanitizeDatabaseError(err) + return nil, fmt.Errorf("error querying distinct source IDs: %w", err) + } + defer rows.Close() + + for rows.Next() { + var sourceID string + if err := rows.Scan(&sourceID); err != nil { + err = dbutil.SanitizeDatabaseError(err) + return nil, fmt.Errorf("error scanning source ID: %w", err) + } + sourceIDs = append(sourceIDs, sourceID) + } + + if err := rows.Err(); err != nil { + err = dbutil.SanitizeDatabaseError(err) + return nil, fmt.Errorf("error iterating source ID rows: %w", err) + } + + return sourceIDs, nil +} + +// applyMcpServerListFilters applies list filters to the query. +func applyMcpServerListFilters(query *gorm.DB, listOptions *models.McpServerListOptions) *gorm.DB { + contextTable := utils.GetTableName(query.Statement.DB, &schema.Context{}) + + if listOptions.Name != nil { + query = query.Where(fmt.Sprintf("%s.name LIKE ?", contextTable), listOptions.Name) + } + + if listOptions.Query != nil && *listOptions.Query != "" { + queryPattern := fmt.Sprintf("%%%s%%", strings.ToLower(*listOptions.Query)) + propertyTable := utils.GetTableName(query.Statement.DB, &schema.ContextProperty{}) + + // Search in name (context table) + nameCondition := fmt.Sprintf("LOWER(%s.name) LIKE ?", contextTable) + + // Search in description, provider properties + propertyCondition := fmt.Sprintf("EXISTS (SELECT 1 FROM %s cp WHERE cp.context_id = %s.id AND cp.name IN (?, ?) AND LOWER(cp.string_value) LIKE ?)", + propertyTable, contextTable) + + query = query.Where(fmt.Sprintf("(%s OR %s)", nameCondition, propertyCondition), + queryPattern, + "description", "provider", queryPattern, + ) + } + + // TextSearch - free-form keyword search across name, description, and provider + if listOptions.TextSearch != nil && *listOptions.TextSearch != "" { + searchPattern := fmt.Sprintf("%%%s%%", strings.ToLower(*listOptions.TextSearch)) + propertyTable := utils.GetTableName(query.Statement.DB, &schema.ContextProperty{}) + + // Search in name (context table) + nameCondition := fmt.Sprintf("LOWER(%s.name) LIKE ?", contextTable) + + // Search in description, provider properties + propertyCondition := fmt.Sprintf("EXISTS (SELECT 1 FROM %s cp WHERE cp.context_id = %s.id AND cp.name IN (?, ?) AND LOWER(cp.string_value) LIKE ?)", + propertyTable, contextTable) + + query = query.Where(fmt.Sprintf("(%s OR %s)", nameCondition, propertyCondition), + searchPattern, + "description", "provider", searchPattern, + ) + } + + // Filter by source IDs + var nonEmptySourceIDs []string + if listOptions.SourceIDs != nil { + for _, sourceID := range *listOptions.SourceIDs { + if sourceID != "" { + nonEmptySourceIDs = append(nonEmptySourceIDs, sourceID) + } + } + } + + if len(nonEmptySourceIDs) > 0 { + propertyTable := utils.GetTableName(query.Statement.DB, &schema.ContextProperty{}) + + joinClause := fmt.Sprintf("JOIN %s cp ON cp.context_id = %s.id", propertyTable, contextTable) + query = query.Joins(joinClause). + Where("cp.name = ? AND cp.string_value IN ?", "source_id", nonEmptySourceIDs) + } + + return query +} + +// mapMcpServerToContext maps an McpServer entity to a Context schema. +func mapMcpServerToContext(server models.McpServer) schema.Context { + attrs := server.GetAttributes() + context := schema.Context{} + + if typeID := server.GetTypeID(); typeID != nil { + context.TypeID = *typeID + } + + if server.GetID() != nil { + context.ID = *server.GetID() + } + + if attrs != nil { + if attrs.Name != nil { + context.Name = *attrs.Name + } + context.ExternalID = attrs.ExternalID + if attrs.CreateTimeSinceEpoch != nil { + context.CreateTimeSinceEpoch = *attrs.CreateTimeSinceEpoch + } + if attrs.LastUpdateTimeSinceEpoch != nil { + context.LastUpdateTimeSinceEpoch = *attrs.LastUpdateTimeSinceEpoch + } + } + + return context +} + +// mapMcpServerToContextProperties maps an McpServer entity to ContextProperty schema. +func mapMcpServerToContextProperties(server models.McpServer, contextID int32) []schema.ContextProperty { + var properties []schema.ContextProperty + + if server.GetProperties() != nil { + for _, prop := range *server.GetProperties() { + properties = append(properties, service.MapPropertiesToContextProperty(prop, contextID, false)) + } + } + + if server.GetCustomProperties() != nil { + for _, prop := range *server.GetCustomProperties() { + properties = append(properties, service.MapPropertiesToContextProperty(prop, contextID, true)) + } + } + + return properties +} + +// mapDataLayerToMcpServer maps database schema to an McpServer entity. +func mapDataLayerToMcpServer(serverCtx schema.Context, propertiesCtx []schema.ContextProperty) models.McpServer { + mcpServer := &models.McpServerImpl{ + ID: &serverCtx.ID, + TypeID: &serverCtx.TypeID, + Attributes: &models.McpServerAttributes{ + Name: &serverCtx.Name, + ExternalID: serverCtx.ExternalID, + CreateTimeSinceEpoch: &serverCtx.CreateTimeSinceEpoch, + LastUpdateTimeSinceEpoch: &serverCtx.LastUpdateTimeSinceEpoch, + }, + } + + properties := []dbmodels.Properties{} + customProperties := []dbmodels.Properties{} + + for _, prop := range propertiesCtx { + mappedProperty := service.MapContextPropertyToProperties(prop) + + if prop.IsCustomProperty { + customProperties = append(customProperties, mappedProperty) + } else { + properties = append(properties, mappedProperty) + } + } + + mcpServer.Properties = &properties + mcpServer.CustomProperties = &customProperties + + return mcpServer +} + +// applyCustomOrdering applies custom ordering logic. +func (r *McpServerRepositoryImpl) applyCustomOrdering(query *gorm.DB, listOptions *models.McpServerListOptions) *gorm.DB { + db := r.GetConfig().DB + contextTable := utils.GetTableName(db, &schema.Context{}) + orderBy := listOptions.GetOrderBy() + + // Handle NAME ordering + if orderBy == "NAME" { + return ApplyNameOrdering(query, contextTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize()) + } + + // Fall back to standard pagination + return r.ApplyStandardPagination(query, listOptions, []models.McpServer{}) +} + +// ApplyStandardPagination overrides the base implementation. +func (r *McpServerRepositoryImpl) ApplyStandardPagination(query *gorm.DB, listOptions *models.McpServerListOptions, entities any) *gorm.DB { + pageSize := listOptions.GetPageSize() + orderBy := listOptions.GetOrderBy() + sortOrder := listOptions.GetSortOrder() + nextPageToken := listOptions.GetNextPageToken() + + pagination := &dbmodels.Pagination{ + PageSize: &pageSize, + OrderBy: &orderBy, + SortOrder: &sortOrder, + NextPageToken: &nextPageToken, + } + + return query.Scopes(scopes.PaginateWithOptions(entities, pagination, r.GetConfig().DB, "Context", CatalogOrderByColumns)) +} + +// createPaginationToken creates a pagination token for the last item. +func (r *McpServerRepositoryImpl) createPaginationToken(lastItem schema.Context, listOptions *models.McpServerListOptions) string { + if listOptions.GetOrderBy() == "NAME" { + return CreateNamePaginationToken(lastItem.ID, &lastItem.Name) + } + + return r.CreateDefaultPaginationToken(lastItem, listOptions) +} + +// McpOrderByColumns are the allowed orderBy columns for MCP servers. +var McpOrderByColumns = map[string]bool{ + "NAME": true, + "CREATE_TIME": true, + "LAST_UPDATE_TIME": true, + "CREATE_TIME_SINCE_EPOCH": true, + "LAST_UPDATE_TIME_SINCE_EPOCH": true, +} + +func init() { + glog.Infof("MCP server repository initialized") +} diff --git a/catalog/internal/db/service/spec.go b/catalog/internal/db/service/spec.go index a9b779440c..399329a0ca 100644 --- a/catalog/internal/db/service/spec.go +++ b/catalog/internal/db/service/spec.go @@ -10,6 +10,8 @@ const ( CatalogModelArtifactTypeName = "kf.CatalogModelArtifact" CatalogMetricsArtifactTypeName = "kf.CatalogMetricsArtifact" CatalogSourceTypeName = "kf.CatalogSource" + McpServerTypeName = "kf.McpServer" + McpServerToolTypeName = "kf.McpServerTool" ) func DatastoreSpec() *datastore.Spec { @@ -33,6 +35,28 @@ func DatastoreSpec() *datastore.Spec { AddString("status"). AddString("error"), ). + AddContext(McpServerTypeName, datastore.NewSpecType(NewMcpServerRepository). + AddString("source_id"). + AddString("description"). + AddString("logo"). + AddString("provider"). + AddString("version"). + AddString("status"). + AddString("transport"). + AddString("category"). + AddStruct("tags"). + AddString("endpoint"). + AddString("documentationUrl"). + AddString("repositoryUrl"). + AddString("sourceCode"). + AddString("readme"). + AddString("publishedDate"). + AddBoolean("verifiedSource"). + AddBoolean("secureEndpoint"). + AddBoolean("sast"). + AddBoolean("readOnlyTools"). + AddStruct("tools"), + ). AddArtifact(CatalogModelArtifactTypeName, datastore.NewSpecType(NewCatalogModelArtifactRepository). AddString("uri"), ). @@ -50,6 +74,7 @@ type Services struct { CatalogMetricsArtifactRepository models.CatalogMetricsArtifactRepository CatalogSourceRepository models.CatalogSourceRepository PropertyOptionsRepository models.PropertyOptionsRepository + McpServerRepository models.McpServerRepository } func NewServices( @@ -59,6 +84,7 @@ func NewServices( catalogMetricsArtifactRepository models.CatalogMetricsArtifactRepository, catalogSourceRepository models.CatalogSourceRepository, propertyOptionsRepository models.PropertyOptionsRepository, + mcpServerRepository models.McpServerRepository, ) Services { return Services{ CatalogModelRepository: catalogModelRepository, @@ -67,5 +93,6 @@ func NewServices( CatalogMetricsArtifactRepository: catalogMetricsArtifactRepository, CatalogSourceRepository: catalogSourceRepository, PropertyOptionsRepository: propertyOptionsRepository, + McpServerRepository: mcpServerRepository, } } diff --git a/catalog/internal/integration/performance_artifacts_test.go b/catalog/internal/integration/performance_artifacts_test.go index 754adfea17..3c487de4c4 100644 --- a/catalog/internal/integration/performance_artifacts_test.go +++ b/catalog/internal/integration/performance_artifacts_test.go @@ -79,6 +79,7 @@ func TestIntegration_PreservedRecommendationAlgorithm(t *testing.T) { metricsArtifactRepo, catalogSourceRepo, service.NewPropertyOptionsRepository(sharedDB), + nil, // McpServerRepository ) provider := catalog.NewDBCatalog(services, nil) @@ -368,6 +369,7 @@ func TestIntegration_ServiceLayerBehavior(t *testing.T) { metricsArtifactRepo, catalogSourceRepo, service.NewPropertyOptionsRepository(sharedDB), + nil, // McpServerRepository ) provider := catalog.NewDBCatalog(services, nil) @@ -539,6 +541,7 @@ func TestIntegration_ConfigurableProperties(t *testing.T) { metricsArtifactRepo, catalogSourceRepo, service.NewPropertyOptionsRepository(sharedDB), + nil, // McpServerRepository ) provider := catalog.NewDBCatalog(services, nil) diff --git a/catalog/internal/mcp/db_mcp_catalog.go b/catalog/internal/mcp/db_mcp_catalog.go new file mode 100644 index 0000000000..58adb06ca9 --- /dev/null +++ b/catalog/internal/mcp/db_mcp_catalog.go @@ -0,0 +1,776 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + "time" + + dbmodels "github.com/kubeflow/model-registry/catalog/internal/db/models" + model "github.com/kubeflow/model-registry/catalog/pkg/openapi" + mrmodels "github.com/kubeflow/model-registry/internal/db/models" +) + +// NamedQueryResolver is a function that returns named query definitions. +type NamedQueryResolver func() map[string]map[string]model.FieldFilter + +// DbMcpCatalogProvider implements MCP server catalog using database storage. +type DbMcpCatalogProvider struct { + repository dbmodels.McpServerRepository + namedQueryResolver NamedQueryResolver +} + +// NewDbMcpCatalogProvider creates a new database-backed MCP catalog provider. +func NewDbMcpCatalogProvider(repository dbmodels.McpServerRepository) *DbMcpCatalogProvider { + return &DbMcpCatalogProvider{ + repository: repository, + } +} + +// SetNamedQueryResolver sets the function to resolve named queries. +func (p *DbMcpCatalogProvider) SetNamedQueryResolver(resolver NamedQueryResolver) { + p.namedQueryResolver = resolver +} + +// ListMcpServers returns all MCP servers with optional filtering by name, q (text search), filterQuery, and namedQuery. +func (p *DbMcpCatalogProvider) ListMcpServers(ctx context.Context, name string, q string, filterQuery string, namedQuery string) ([]model.McpServer, error) { + listOptions := dbmodels.McpServerListOptions{} + if name != "" { + listOptions.Query = &name + } + if q != "" { + listOptions.TextSearch = &q + } + + // Resolve named query to filter conditions + resolvedFilterQuery := filterQuery + if namedQuery != "" && p.namedQueryResolver != nil { + namedQueries := p.namedQueryResolver() + if queryFilters, exists := namedQueries[namedQuery]; exists { + // Convert named query fields to SQL-like filter conditions + namedFilterQuery := convertNamedQueryToFilterQuery(queryFilters) + if namedFilterQuery != "" { + if resolvedFilterQuery != "" { + // Combine with existing filter query using AND + resolvedFilterQuery = fmt.Sprintf("(%s) AND (%s)", resolvedFilterQuery, namedFilterQuery) + } else { + resolvedFilterQuery = namedFilterQuery + } + } + } else { + return nil, fmt.Errorf("named query %q not found", namedQuery) + } + } + + if resolvedFilterQuery != "" { + // Transform license display names to SPDX identifiers before filtering + transformedQuery := transformLicenseInFilterQuery(resolvedFilterQuery) + listOptions.FilterQuery = &transformedQuery + } + + result, err := p.repository.List(listOptions) + if err != nil { + return nil, fmt.Errorf("failed to list MCP servers: %w", err) + } + + servers := make([]model.McpServer, 0, len(result.Items)) + for _, item := range result.Items { + server := convertDbToApiMcpServer(item) + servers = append(servers, server) + } + + return servers, nil +} + +// GetMcpServer returns a specific MCP server by ID. +func (p *DbMcpCatalogProvider) GetMcpServer(ctx context.Context, serverId string) (*model.McpServer, error) { + // First try to parse as integer ID + if id, err := strconv.ParseInt(serverId, 10, 32); err == nil { + dbServer, err := p.repository.GetByID(int32(id)) + if err != nil { + if err.Error() == "MCP server by id not found" { + return nil, nil + } + return nil, fmt.Errorf("failed to get MCP server by ID: %w", err) + } + server := convertDbToApiMcpServer(dbServer) + return &server, nil + } + + // Otherwise try by name + dbServer, err := p.repository.GetByName(serverId) + if err != nil { + if err.Error() == "MCP server by id not found" { + return nil, nil + } + return nil, fmt.Errorf("failed to get MCP server by name: %w", err) + } + + server := convertDbToApiMcpServer(dbServer) + return &server, nil +} + +// convertDbToApiMcpServer converts a database MCP server to API model. +func convertDbToApiMcpServer(dbServer dbmodels.McpServer) model.McpServer { + server := model.McpServer{} + + // Get basic attributes + attrs := dbServer.GetAttributes() + if attrs != nil { + if attrs.Name != nil { + server.Name = *attrs.Name + } + if attrs.CreateTimeSinceEpoch != nil { + t := time.UnixMilli(*attrs.CreateTimeSinceEpoch) + server.LastUpdated = &t + } + } + + // Use database ID as the server ID + if id := dbServer.GetID(); id != nil { + server.Id = strconv.Itoa(int(*id)) + } + + // Initialize customProperties map for Phase 2.11 alignment + customProperties := make(map[string]model.MetadataValue) + + // Convert properties + props := dbServer.GetProperties() + if props != nil { + for _, prop := range *props { + convertPropertyToApiField(&server, prop) + // Also populate customProperties for Phase 2.11 alignment + addPropertyToCustomProperties(customProperties, prop) + } + } + + // Populate customProperties if any entries were added + if len(customProperties) > 0 { + server.CustomProperties = customProperties + } + + // Derive transports from endpoints for remote servers if not explicitly set + if len(server.Transports) == 0 { + server.Transports = deriveTransportsFromEndpoints(&server) + } + + return server +} + +// deriveTransportsFromEndpoints derives transport types from the endpoints object. +// For remote servers, transports are derived from available endpoints. +// For local servers, defaults to ["stdio"]. +func deriveTransportsFromEndpoints(server *model.McpServer) []model.McpTransportType { + // For remote servers, derive from endpoints + if server.DeploymentMode != nil && *server.DeploymentMode == model.MCPDEPLOYMENTMODE_REMOTE { + transports := []model.McpTransportType{} + if server.Endpoints != nil { + if server.Endpoints.Http != nil && *server.Endpoints.Http != "" { + transports = append(transports, model.MCPTRANSPORTTYPE_HTTP) + } + if server.Endpoints.Sse != nil && *server.Endpoints.Sse != "" { + transports = append(transports, model.MCPTRANSPORTTYPE_SSE) + } + } + // If no endpoints found, default to http for remote servers + if len(transports) == 0 { + return []model.McpTransportType{model.MCPTRANSPORTTYPE_HTTP} + } + return transports + } + + // For local servers, default to stdio + return []model.McpTransportType{model.MCPTRANSPORTTYPE_STDIO} +} + +// convertPropertyToApiField maps a database property to an API server field. +func convertPropertyToApiField(server *model.McpServer, prop mrmodels.Properties) { + switch prop.Name { + case "source_id": + if prop.StringValue != nil { + server.SourceId = prop.StringValue + } + case "description": + if prop.StringValue != nil { + server.Description = prop.StringValue + } + case "logo": + if prop.StringValue != nil { + server.Logo = prop.StringValue + } + case "provider": + if prop.StringValue != nil { + server.Provider = prop.StringValue + } + case "version": + if prop.StringValue != nil { + server.Version = prop.StringValue + } + case "license": + if prop.StringValue != nil { + displayName := formatLicenseDisplayName(*prop.StringValue) + server.License = &displayName + } + case "license_link": + if prop.StringValue != nil { + server.LicenseLink = prop.StringValue + } + case "transports": + if prop.StringValue != nil { + var transports []string + if err := json.Unmarshal([]byte(*prop.StringValue), &transports); err == nil { + server.Transports = convertStringsToTransports(transports) + } + } + case "artifacts": + if prop.StringValue != nil { + var artifacts []yamlMcpArtifact + if err := json.Unmarshal([]byte(*prop.StringValue), &artifacts); err == nil { + server.Artifacts = make([]model.McpArtifact, 0, len(artifacts)) + for _, a := range artifacts { + artifact := model.McpArtifact{ + Uri: a.Uri, + } + if a.CreateTimeSinceEpoch != "" { + artifact.CreateTimeSinceEpoch = &a.CreateTimeSinceEpoch + } + if a.LastUpdateTimeSinceEpoch != "" { + artifact.LastUpdateTimeSinceEpoch = &a.LastUpdateTimeSinceEpoch + } + server.Artifacts = append(server.Artifacts, artifact) + } + } + } + case "documentationUrl": + if prop.StringValue != nil { + server.DocumentationUrl = prop.StringValue + } + case "repositoryUrl": + if prop.StringValue != nil { + server.RepositoryUrl = prop.StringValue + } + case "sourceCode": + if prop.StringValue != nil { + server.SourceCode = prop.StringValue + } + case "readme": + if prop.StringValue != nil { + server.Readme = prop.StringValue + } + case "publishedDate": + if prop.StringValue != nil { + server.PublishedDate = prop.StringValue + } + case "tags": + if prop.StringValue != nil { + var tags []string + if err := json.Unmarshal([]byte(*prop.StringValue), &tags); err == nil { + server.Tags = tags + } + } + case "tools": + if prop.StringValue != nil { + var tools []yamlMcpTool + if err := json.Unmarshal([]byte(*prop.StringValue), &tools); err == nil { + server.Tools = make([]model.McpTool, 0, len(tools)) + for _, t := range tools { + server.Tools = append(server.Tools, convertYamlToolToApiTool(t)) + } + } + } + case "verifiedSource": + if prop.BoolValue != nil { + if server.SecurityIndicators == nil { + server.SecurityIndicators = &model.McpSecurityIndicator{} + } + server.SecurityIndicators.VerifiedSource = prop.BoolValue + } + case "secureEndpoint": + if prop.BoolValue != nil { + if server.SecurityIndicators == nil { + server.SecurityIndicators = &model.McpSecurityIndicator{} + } + server.SecurityIndicators.SecureEndpoint = prop.BoolValue + } + case "sast": + if prop.BoolValue != nil { + if server.SecurityIndicators == nil { + server.SecurityIndicators = &model.McpSecurityIndicator{} + } + server.SecurityIndicators.Sast = prop.BoolValue + } + case "readOnlyTools": + if prop.BoolValue != nil { + if server.SecurityIndicators == nil { + server.SecurityIndicators = &model.McpSecurityIndicator{} + } + server.SecurityIndicators.ReadOnlyTools = prop.BoolValue + } + case "deploymentMode": + if prop.StringValue != nil { + deploymentMode := convertStringToDeploymentMode(*prop.StringValue) + server.DeploymentMode = &deploymentMode + } + case "endpoints": + if prop.StringValue != nil { + var endpoints yamlMcpEndpoints + if err := json.Unmarshal([]byte(*prop.StringValue), &endpoints); err == nil { + server.Endpoints = convertYamlEndpointsToApiEndpoints(&endpoints) + } + } + } +} + +// addPropertyToCustomProperties adds a database property to the customProperties map. +// This function implements Phase 2.11 alignment: +// - Tags are stored as MetadataStringValue entries with empty string_value (label pattern) +// - Security indicators are stored as MetadataBoolValue entries +// - Other properties are stored with their appropriate MetadataValue types +func addPropertyToCustomProperties(customProperties map[string]model.MetadataValue, prop mrmodels.Properties) { + metadataBool := "MetadataBoolValue" + metadataString := "MetadataStringValue" + + switch prop.Name { + case "tags": + // Convert tags JSON array to individual entries with empty string_value (label pattern) + if prop.StringValue != nil { + var tags []string + if err := json.Unmarshal([]byte(*prop.StringValue), &tags); err == nil { + emptyString := "" + for _, tag := range tags { + customProperties[tag] = model.MetadataStringValueAsMetadataValue(&model.MetadataStringValue{ + StringValue: emptyString, + MetadataType: metadataString, + }) + } + } + } + case "verifiedSource", "secureEndpoint", "sast", "readOnlyTools": + // Security indicators stored as MetadataBoolValue + if prop.BoolValue != nil { + customProperties[prop.Name] = model.MetadataBoolValueAsMetadataValue(&model.MetadataBoolValue{ + BoolValue: *prop.BoolValue, + MetadataType: metadataBool, + }) + } + case "provider", "license", "version", "deploymentMode": + // String properties that might be useful for filtering + if prop.StringValue != nil { + customProperties[prop.Name] = model.MetadataStringValueAsMetadataValue(&model.MetadataStringValue{ + StringValue: *prop.StringValue, + MetadataType: metadataString, + }) + } + // Skip properties that are already first-class fields or internal + case "description", "logo", "documentationUrl", "repositoryUrl", "sourceCode", + "readme", "publishedDate", "source_id", "tools", "transports", "endpoints": + // These are already first-class fields or handled elsewhere + return + } +} + +// convertYamlToolToApiTool converts a YAML tool to API model. +func convertYamlToolToApiTool(t yamlMcpTool) model.McpTool { + tool := model.McpTool{ + Name: t.Name, + AccessType: convertStringToAccessType(t.AccessType), + } + + if t.Description != "" { + tool.Description = &t.Description + } + + if len(t.Parameters) > 0 { + tool.Parameters = make([]model.McpToolParameter, 0, len(t.Parameters)) + for _, p := range t.Parameters { + param := model.McpToolParameter{ + Name: p.Name, + Type: p.Type, + Required: p.Required, + } + if p.Description != "" { + param.Description = &p.Description + } + tool.Parameters = append(tool.Parameters, param) + } + } + + // Set revoked status (defaults to false) + tool.Revoked = &t.Revoked + if t.RevokedReason != "" { + tool.RevokedReason = &t.RevokedReason + } + + return tool +} + +// convertStringToTransport converts a string to McpTransportType. +func convertStringToTransport(transport string) model.McpTransportType { + switch transport { + case "http": + return model.MCPTRANSPORTTYPE_HTTP + case "sse": + return model.MCPTRANSPORTTYPE_SSE + case "stdio": + return model.MCPTRANSPORTTYPE_STDIO + default: + return model.MCPTRANSPORTTYPE_STDIO + } +} + +// convertStringsToTransports converts a slice of strings to McpTransportType slice. +func convertStringsToTransports(transports []string) []model.McpTransportType { + result := make([]model.McpTransportType, 0, len(transports)) + for _, t := range transports { + result = append(result, convertStringToTransport(t)) + } + return result +} + +// convertStringToAccessType converts a string to McpToolAccessType. +func convertStringToAccessType(accessType string) model.McpToolAccessType { + switch accessType { + case "read_only": + return model.MCPTOOLACCESSTYPE_READ_ONLY + case "read_write": + return model.MCPTOOLACCESSTYPE_READ_WRITE + case "execute": + return model.MCPTOOLACCESSTYPE_EXECUTE + default: + return model.MCPTOOLACCESSTYPE_READ_ONLY + } +} + +// convertStringToDeploymentMode converts a string to McpDeploymentMode. +func convertStringToDeploymentMode(mode string) model.McpDeploymentMode { + switch mode { + case "remote": + return model.MCPDEPLOYMENTMODE_REMOTE + case "local": + return model.MCPDEPLOYMENTMODE_LOCAL + default: + return model.MCPDEPLOYMENTMODE_LOCAL + } +} + +// convertYamlEndpointsToApiEndpoints converts YAML endpoints to API model. +func convertYamlEndpointsToApiEndpoints(endpoints *yamlMcpEndpoints) *model.McpEndpoints { + if endpoints == nil { + return nil + } + apiEndpoints := &model.McpEndpoints{} + if endpoints.Http != "" { + apiEndpoints.Http = &endpoints.Http + } + if endpoints.Sse != "" { + apiEndpoints.Sse = &endpoints.Sse + } + return apiEndpoints +} + +// GetFilterOptions returns all available filter options for MCP servers. +// This includes field names and available values for each filterable field. +func (p *DbMcpCatalogProvider) GetFilterOptions(ctx context.Context) (*model.FilterOptionsList, error) { + // Get all servers (without filtering) to extract unique values + allServers, err := p.ListMcpServers(ctx, "", "", "", "") + if err != nil { + return nil, fmt.Errorf("failed to list MCP servers for filter options: %w", err) + } + + // Use maps to track unique values for each filterable field + providers := make(map[string]struct{}) + licenses := make(map[string]struct{}) + tags := make(map[string]struct{}) + transports := make(map[string]struct{}) + deploymentModes := make(map[string]struct{}) + + for _, server := range allServers { + // Provider + if server.Provider != nil && *server.Provider != "" { + providers[*server.Provider] = struct{}{} + } + + // License (display name) + if server.License != nil && *server.License != "" { + licenses[*server.License] = struct{}{} + } + + // Tags + for _, tag := range server.Tags { + if tag != "" { + tags[tag] = struct{}{} + } + } + + // Transports + for _, transport := range server.Transports { + transports[string(transport)] = struct{}{} + } + + // Deployment mode + if server.DeploymentMode != nil { + deploymentModes[string(*server.DeploymentMode)] = struct{}{} + } + } + + // Build filter options map + options := make(map[string]model.FilterOption) + + if len(providers) > 0 { + options["provider"] = model.FilterOption{ + Type: "string", + Values: mapKeysToInterface(providers), + } + } + + if len(licenses) > 0 { + options["license"] = model.FilterOption{ + Type: "string", + Values: mapKeysToInterface(licenses), + } + } + + if len(tags) > 0 { + options["tags"] = model.FilterOption{ + Type: "string", + Values: mapKeysToInterface(tags), + } + } + + if len(transports) > 0 { + options["transports"] = model.FilterOption{ + Type: "string", + Values: mapKeysToInterface(transports), + } + } + + if len(deploymentModes) > 0 { + options["deploymentMode"] = model.FilterOption{ + Type: "string", + Values: mapKeysToInterface(deploymentModes), + } + } + + result := &model.FilterOptionsList{ + Filters: &options, + } + + // Include named queries if resolver is configured + if p.namedQueryResolver != nil { + namedQueries := p.namedQueryResolver() + if len(namedQueries) > 0 { + result.NamedQueries = &namedQueries + } + } + + return result, nil +} + +// mapKeysToInterface converts a map's keys to a sorted slice of interface{}. +func mapKeysToInterface(m map[string]struct{}) []interface{} { + // Extract keys + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + // Sort keys for deterministic output + sort.Strings(keys) + + // Convert to interface slice + result := make([]interface{}, len(keys)) + for i, k := range keys { + result[i] = k + } + return result +} + +// spdxToDisplayName maps SPDX license identifiers to user-friendly display names. +var spdxToDisplayName = map[string]string{ + "apache-2.0": "Apache 2.0", + "mit": "MIT", + "gpl-2.0": "GPL 2.0", + "gpl-3.0": "GPL 3.0", + "lgpl-2.1": "LGPL 2.1", + "lgpl-3.0": "LGPL 3.0", + "bsd-2-clause": "BSD 2-Clause", + "bsd-3-clause": "BSD 3-Clause", + "mpl-2.0": "MPL 2.0", + "elastic-2.0": "Elastic 2.0", + "cc-by-4.0": "CC BY 4.0", + "cc-by-sa-4.0": "CC BY-SA 4.0", + "cc0-1.0": "CC0 1.0", + "unlicense": "Unlicense", + "isc": "ISC", + "wtfpl": "WTFPL", + "gemma": "Gemma", + "llama-3.1": "Llama 3.1", + "llama-3.3": "Llama 3.3", + "llama3.1": "Llama 3.1", + "llama3.3": "Llama 3.3", + "llama4": "Llama 4", + "modified-mit": "Modified MIT", +} + +// formatLicenseDisplayName converts an SPDX license identifier to a user-friendly display name. +func formatLicenseDisplayName(spdxLicense string) string { + if displayName, exists := spdxToDisplayName[spdxLicense]; exists { + return displayName + } + // Return the original value if not found in the mapping + return spdxLicense +} + +// displayNameToSpdx is the reverse mapping from display names to SPDX identifiers. +// This is built once at init time from spdxToDisplayName. +var displayNameToSpdx map[string]string + +func init() { + displayNameToSpdx = make(map[string]string, len(spdxToDisplayName)) + for spdx, display := range spdxToDisplayName { + displayNameToSpdx[display] = spdx + } +} + +// transformLicenseInFilterQuery transforms license display names in a filter query to SPDX identifiers. +// This handles filter queries like "license='Apache 2.0'" and converts them to "license='apache-2.0'". +func transformLicenseInFilterQuery(filterQuery string) string { + if filterQuery == "" { + return filterQuery + } + + result := filterQuery + + // Replace each display name with its SPDX identifier + for display, spdx := range displayNameToSpdx { + // Handle single quotes: license='Apache 2.0' + oldSingle := "'" + display + "'" + newSingle := "'" + spdx + "'" + result = strings.ReplaceAll(result, oldSingle, newSingle) + } + + return result +} + +// convertNamedQueryToFilterQuery converts named query field filters to a SQL-like filter query string. +// This enables named queries to work with the existing filterQuery parser in the database layer. +func convertNamedQueryToFilterQuery(fieldFilters map[string]model.FieldFilter) string { + if len(fieldFilters) == 0 { + return "" + } + + var conditions []string + + for fieldName, filter := range fieldFilters { + condition := convertFieldFilterToCondition(fieldName, filter) + if condition != "" { + conditions = append(conditions, condition) + } + } + + if len(conditions) == 0 { + return "" + } + + // Join all conditions with AND + return strings.Join(conditions, " AND ") +} + +// convertFieldFilterToCondition converts a single field filter to a SQL-like condition. +func convertFieldFilterToCondition(fieldName string, filter model.FieldFilter) string { + operator := strings.ToUpper(filter.Operator) + + switch operator { + case "=", "EQUALS": + return formatEqualityCondition(fieldName, "=", filter.Value) + case "!=", "<>", "NOT_EQUALS": + return formatEqualityCondition(fieldName, "!=", filter.Value) + case ">", ">=", "<", "<=": + return formatComparisonCondition(fieldName, operator, filter.Value) + case "IN", "ANYOF": + return formatInCondition(fieldName, filter.Value) + case "LIKE", "ILIKE": + return formatLikeCondition(fieldName, operator, filter.Value) + default: + // Default to equality + return formatEqualityCondition(fieldName, "=", filter.Value) + } +} + +// formatEqualityCondition formats an equality/inequality condition. +func formatEqualityCondition(fieldName, operator string, value interface{}) string { + switch v := value.(type) { + case string: + return fmt.Sprintf("%s %s '%s'", fieldName, operator, escapeString(v)) + case bool: + return fmt.Sprintf("%s %s %t", fieldName, operator, v) + case float64: + // JSON numbers come as float64 + if float64(int64(v)) == v { + return fmt.Sprintf("%s %s %d", fieldName, operator, int64(v)) + } + return fmt.Sprintf("%s %s %f", fieldName, operator, v) + case int, int32, int64: + return fmt.Sprintf("%s %s %v", fieldName, operator, v) + default: + return fmt.Sprintf("%s %s '%v'", fieldName, operator, v) + } +} + +// formatComparisonCondition formats a comparison condition. +func formatComparisonCondition(fieldName, operator string, value interface{}) string { + switch v := value.(type) { + case float64: + if float64(int64(v)) == v { + return fmt.Sprintf("%s %s %d", fieldName, operator, int64(v)) + } + return fmt.Sprintf("%s %s %f", fieldName, operator, v) + case int, int32, int64: + return fmt.Sprintf("%s %s %v", fieldName, operator, v) + default: + return fmt.Sprintf("%s %s %v", fieldName, operator, v) + } +} + +// formatInCondition formats an IN condition. +func formatInCondition(fieldName string, value interface{}) string { + switch v := value.(type) { + case []interface{}: + values := make([]string, 0, len(v)) + for _, item := range v { + switch itemVal := item.(type) { + case string: + values = append(values, fmt.Sprintf("'%s'", escapeString(itemVal))) + default: + values = append(values, fmt.Sprintf("%v", itemVal)) + } + } + return fmt.Sprintf("%s IN (%s)", fieldName, strings.Join(values, ", ")) + case []string: + values := make([]string, 0, len(v)) + for _, item := range v { + values = append(values, fmt.Sprintf("'%s'", escapeString(item))) + } + return fmt.Sprintf("%s IN (%s)", fieldName, strings.Join(values, ", ")) + default: + return formatEqualityCondition(fieldName, "=", value) + } +} + +// formatLikeCondition formats a LIKE/ILIKE condition. +func formatLikeCondition(fieldName, operator string, value interface{}) string { + switch v := value.(type) { + case string: + return fmt.Sprintf("%s %s '%s'", fieldName, operator, escapeString(v)) + default: + return fmt.Sprintf("%s %s '%v'", fieldName, operator, v) + } +} + +// escapeString escapes single quotes in a string for use in SQL-like conditions. +func escapeString(s string) string { + return strings.ReplaceAll(s, "'", "''") +} diff --git a/catalog/internal/mcp/db_mcp_catalog_test.go b/catalog/internal/mcp/db_mcp_catalog_test.go new file mode 100644 index 0000000000..aba297a59b --- /dev/null +++ b/catalog/internal/mcp/db_mcp_catalog_test.go @@ -0,0 +1,553 @@ +package mcp + +import ( + "testing" + + model "github.com/kubeflow/model-registry/catalog/pkg/openapi" +) + +func TestConvertNamedQueryToFilterQuery(t *testing.T) { + tests := []struct { + name string + fieldFilters map[string]model.FieldFilter + expectedResult string + }{ + { + name: "empty filters", + fieldFilters: map[string]model.FieldFilter{}, + expectedResult: "", + }, + { + name: "nil filters", + fieldFilters: nil, + expectedResult: "", + }, + { + name: "single string equality", + fieldFilters: map[string]model.FieldFilter{ + "provider": {Operator: "=", Value: "anthropic"}, + }, + expectedResult: "provider = 'anthropic'", + }, + { + name: "single boolean value", + fieldFilters: map[string]model.FieldFilter{ + "verifiedSource": {Operator: "=", Value: true}, + }, + expectedResult: "verifiedSource = true", + }, + { + name: "single integer value", + fieldFilters: map[string]model.FieldFilter{ + "toolCount": {Operator: ">=", Value: float64(5)}, + }, + expectedResult: "toolCount >= 5", + }, + { + name: "IN operator with string array", + fieldFilters: map[string]model.FieldFilter{ + "tags": {Operator: "IN", Value: []interface{}{"security", "verified"}}, + }, + expectedResult: "tags IN ('security', 'verified')", + }, + { + name: "LIKE operator", + fieldFilters: map[string]model.FieldFilter{ + "name": {Operator: "LIKE", Value: "%github%"}, + }, + expectedResult: "name LIKE '%github%'", + }, + { + name: "not equals operator", + fieldFilters: map[string]model.FieldFilter{ + "status": {Operator: "!=", Value: "deprecated"}, + }, + expectedResult: "status != 'deprecated'", + }, + { + name: "ANYOF operator (alias for IN)", + fieldFilters: map[string]model.FieldFilter{ + "deploymentMode": {Operator: "ANYOF", Value: []interface{}{"remote", "local"}}, + }, + expectedResult: "deploymentMode IN ('remote', 'local')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertNamedQueryToFilterQuery(tt.fieldFilters) + if result != tt.expectedResult { + t.Errorf("convertNamedQueryToFilterQuery() = %q, want %q", result, tt.expectedResult) + } + }) + } +} + +func TestConvertFieldFilterToCondition(t *testing.T) { + tests := []struct { + name string + fieldName string + filter model.FieldFilter + expectedResult string + }{ + { + name: "string equality with = operator", + fieldName: "provider", + filter: model.FieldFilter{Operator: "=", Value: "anthropic"}, + expectedResult: "provider = 'anthropic'", + }, + { + name: "string equality with EQUALS operator", + fieldName: "provider", + filter: model.FieldFilter{Operator: "EQUALS", Value: "openai"}, + expectedResult: "provider = 'openai'", + }, + { + name: "string not equals with != operator", + fieldName: "status", + filter: model.FieldFilter{Operator: "!=", Value: "deprecated"}, + expectedResult: "status != 'deprecated'", + }, + { + name: "string not equals with NOT_EQUALS operator", + fieldName: "status", + filter: model.FieldFilter{Operator: "NOT_EQUALS", Value: "deprecated"}, + expectedResult: "status != 'deprecated'", + }, + { + name: "string not equals with <> operator", + fieldName: "status", + filter: model.FieldFilter{Operator: "<>", Value: "deprecated"}, + expectedResult: "status != 'deprecated'", + }, + { + name: "boolean true", + fieldName: "verifiedSource", + filter: model.FieldFilter{Operator: "=", Value: true}, + expectedResult: "verifiedSource = true", + }, + { + name: "boolean false", + fieldName: "verifiedSource", + filter: model.FieldFilter{Operator: "=", Value: false}, + expectedResult: "verifiedSource = false", + }, + { + name: "greater than integer", + fieldName: "toolCount", + filter: model.FieldFilter{Operator: ">", Value: float64(10)}, + expectedResult: "toolCount > 10", + }, + { + name: "greater than or equal", + fieldName: "toolCount", + filter: model.FieldFilter{Operator: ">=", Value: float64(5)}, + expectedResult: "toolCount >= 5", + }, + { + name: "less than", + fieldName: "latency", + filter: model.FieldFilter{Operator: "<", Value: float64(100)}, + expectedResult: "latency < 100", + }, + { + name: "less than or equal", + fieldName: "latency", + filter: model.FieldFilter{Operator: "<=", Value: float64(50)}, + expectedResult: "latency <= 50", + }, + { + name: "float value", + fieldName: "score", + filter: model.FieldFilter{Operator: ">=", Value: float64(0.95)}, + expectedResult: "score >= 0.950000", + }, + { + name: "IN operator with interface array", + fieldName: "tags", + filter: model.FieldFilter{ + Operator: "IN", + Value: []interface{}{"security", "verified", "production"}, + }, + expectedResult: "tags IN ('security', 'verified', 'production')", + }, + { + name: "IN operator with string array", + fieldName: "transports", + filter: model.FieldFilter{ + Operator: "IN", + Value: []string{"http", "sse"}, + }, + expectedResult: "transports IN ('http', 'sse')", + }, + { + name: "LIKE operator", + fieldName: "name", + filter: model.FieldFilter{Operator: "LIKE", Value: "%github%"}, + expectedResult: "name LIKE '%github%'", + }, + { + name: "ILIKE operator (case insensitive)", + fieldName: "description", + filter: model.FieldFilter{Operator: "ILIKE", Value: "%ai%"}, + expectedResult: "description ILIKE '%ai%'", + }, + { + name: "default to equality for unknown operator", + fieldName: "field", + filter: model.FieldFilter{Operator: "UNKNOWN", Value: "value"}, + expectedResult: "field = 'value'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertFieldFilterToCondition(tt.fieldName, tt.filter) + if result != tt.expectedResult { + t.Errorf("convertFieldFilterToCondition() = %q, want %q", result, tt.expectedResult) + } + }) + } +} + +func TestEscapeString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no special characters", + input: "hello world", + expected: "hello world", + }, + { + name: "single quote", + input: "it's a test", + expected: "it''s a test", + }, + { + name: "multiple single quotes", + input: "it's Bob's test", + expected: "it''s Bob''s test", + }, + { + name: "empty string", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := escapeString(tt.input) + if result != tt.expected { + t.Errorf("escapeString() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestFormatLicenseDisplayName(t *testing.T) { + tests := []struct { + name string + spdxLicense string + expectedName string + }{ + { + name: "apache-2.0", + spdxLicense: "apache-2.0", + expectedName: "Apache 2.0", + }, + { + name: "mit", + spdxLicense: "mit", + expectedName: "MIT", + }, + { + name: "gpl-3.0", + spdxLicense: "gpl-3.0", + expectedName: "GPL 3.0", + }, + { + name: "unknown license", + spdxLicense: "custom-license", + expectedName: "custom-license", + }, + { + name: "llama-3.1", + spdxLicense: "llama-3.1", + expectedName: "Llama 3.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatLicenseDisplayName(tt.spdxLicense) + if result != tt.expectedName { + t.Errorf("formatLicenseDisplayName() = %q, want %q", result, tt.expectedName) + } + }) + } +} + +func TestTransformLicenseInFilterQuery(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "no license filter", + input: "provider='anthropic'", + expected: "provider='anthropic'", + }, + { + name: "single license display name", + input: "license='Apache 2.0'", + expected: "license='apache-2.0'", + }, + { + name: "MIT license", + input: "license='MIT'", + expected: "license='mit'", + }, + { + name: "license in complex query", + input: "provider='anthropic' AND license='Apache 2.0' AND tags='ai'", + expected: "provider='anthropic' AND license='apache-2.0' AND tags='ai'", + }, + { + name: "license IN clause", + input: "license IN ('Apache 2.0','MIT')", + expected: "license IN ('apache-2.0','mit')", + }, + { + name: "unknown license unchanged", + input: "license='Custom License'", + expected: "license='Custom License'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := transformLicenseInFilterQuery(tt.input) + if result != tt.expected { + t.Errorf("transformLicenseInFilterQuery() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestMapKeysToInterface(t *testing.T) { + tests := []struct { + name string + input map[string]struct{} + expected []interface{} + }{ + { + name: "multiple keys", + input: map[string]struct{}{ + "apple": {}, + "banana": {}, + "cherry": {}, + }, + expected: []interface{}{"apple", "banana", "cherry"}, // sorted + }, + { + name: "empty map", + input: map[string]struct{}{}, + expected: []interface{}{}, + }, + { + name: "single key", + input: map[string]struct{}{ + "only": {}, + }, + expected: []interface{}{"only"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mapKeysToInterface(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("mapKeysToInterface() returned %d items, want %d", len(result), len(tt.expected)) + return + } + for i, v := range result { + if v != tt.expected[i] { + t.Errorf("mapKeysToInterface()[%d] = %v, want %v", i, v, tt.expected[i]) + } + } + }) + } +} + +func TestConvertStringToTransport(t *testing.T) { + tests := []struct { + name string + transport string + expected model.McpTransportType + }{ + { + name: "http", + transport: "http", + expected: model.MCPTRANSPORTTYPE_HTTP, + }, + { + name: "sse", + transport: "sse", + expected: model.MCPTRANSPORTTYPE_SSE, + }, + { + name: "stdio", + transport: "stdio", + expected: model.MCPTRANSPORTTYPE_STDIO, + }, + { + name: "unknown defaults to stdio", + transport: "websocket", + expected: model.MCPTRANSPORTTYPE_STDIO, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertStringToTransport(tt.transport) + if result != tt.expected { + t.Errorf("convertStringToTransport() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestConvertStringToAccessType(t *testing.T) { + tests := []struct { + name string + accessType string + expected model.McpToolAccessType + }{ + { + name: "read_only", + accessType: "read_only", + expected: model.MCPTOOLACCESSTYPE_READ_ONLY, + }, + { + name: "read_write", + accessType: "read_write", + expected: model.MCPTOOLACCESSTYPE_READ_WRITE, + }, + { + name: "execute", + accessType: "execute", + expected: model.MCPTOOLACCESSTYPE_EXECUTE, + }, + { + name: "unknown defaults to read_only", + accessType: "unknown", + expected: model.MCPTOOLACCESSTYPE_READ_ONLY, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertStringToAccessType(tt.accessType) + if result != tt.expected { + t.Errorf("convertStringToAccessType() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestConvertStringToDeploymentMode(t *testing.T) { + tests := []struct { + name string + mode string + expected model.McpDeploymentMode + }{ + { + name: "remote", + mode: "remote", + expected: model.MCPDEPLOYMENTMODE_REMOTE, + }, + { + name: "local", + mode: "local", + expected: model.MCPDEPLOYMENTMODE_LOCAL, + }, + { + name: "unknown defaults to local", + mode: "hybrid", + expected: model.MCPDEPLOYMENTMODE_LOCAL, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertStringToDeploymentMode(tt.mode) + if result != tt.expected { + t.Errorf("convertStringToDeploymentMode() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestNamedQueryResolver(t *testing.T) { + // Test that SetNamedQueryResolver and the resolver work correctly + provider := NewDbMcpCatalogProvider(nil) // nil repository is fine for this test + + if provider.namedQueryResolver != nil { + t.Error("namedQueryResolver should be nil initially") + } + + // Set a resolver + testQueries := map[string]map[string]model.FieldFilter{ + "production_ready": { + "verifiedSource": {Operator: "=", Value: true}, + "provider": {Operator: "IN", Value: []interface{}{"anthropic", "openai"}}, + }, + } + + provider.SetNamedQueryResolver(func() map[string]map[string]model.FieldFilter { + return testQueries + }) + + if provider.namedQueryResolver == nil { + t.Error("namedQueryResolver should be set after SetNamedQueryResolver") + } + + // Verify the resolver returns the expected queries + result := provider.namedQueryResolver() + if len(result) != 1 { + t.Errorf("namedQueryResolver() returned %d queries, want 1", len(result)) + } + + if _, exists := result["production_ready"]; !exists { + t.Error("namedQueryResolver() should contain 'production_ready' query") + } +} + +func TestMultipleConditionsJoin(t *testing.T) { + // Test that multiple field filters are joined with AND + fieldFilters := map[string]model.FieldFilter{ + "verifiedSource": {Operator: "=", Value: true}, + } + + result := convertNamedQueryToFilterQuery(fieldFilters) + + // With a single filter, there should be no AND + if result == "" { + t.Error("convertNamedQueryToFilterQuery() returned empty string for non-empty filters") + } + + // The result should contain the condition + if result != "verifiedSource = true" { + t.Errorf("convertNamedQueryToFilterQuery() = %q, want %q", result, "verifiedSource = true") + } +} diff --git a/catalog/internal/mcp/yaml_mcp_catalog.go b/catalog/internal/mcp/yaml_mcp_catalog.go new file mode 100644 index 0000000000..be518c6c43 --- /dev/null +++ b/catalog/internal/mcp/yaml_mcp_catalog.go @@ -0,0 +1,413 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/golang/glog" + "github.com/kubeflow/model-registry/catalog/internal/common" + dbmodels "github.com/kubeflow/model-registry/catalog/internal/db/models" + "github.com/kubeflow/model-registry/internal/db/models" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const ( + yamlCatalogPathKey = "yamlCatalogPath" +) + +// McpServerProviderRecord contains one MCP server and its associated tools. +type McpServerProviderRecord struct { + Server dbmodels.McpServer + Tools []dbmodels.McpServerTool +} + +// McpServerProviderFunc emits MCP servers and related data in the channel it returns. +type McpServerProviderFunc func(ctx context.Context, source *McpSource, reldir string) (<-chan McpServerProviderRecord, error) + +// McpSource represents a catalog source for MCP servers. +type McpSource struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled *bool `json:"enabled,omitempty"` + Labels []string `json:"labels,omitempty"` + Properties map[string]any `json:"properties,omitempty"` + Origin string `json:"-" yaml:"-"` + + // IncludedServers is an optional list of glob patterns for MCP servers to include. + // If specified, only servers matching at least one pattern will be included. + // Pattern syntax: Only '*' wildcard is supported, patterns are case-insensitive. + IncludedServers []string `json:"includedServers,omitempty" yaml:"includedServers,omitempty"` + + // ExcludedServers is an optional list of glob patterns for MCP servers to exclude. + // Servers matching any pattern will be excluded even if they match an includedServers pattern. + // Exclusions take precedence over inclusions. + ExcludedServers []string `json:"excludedServers,omitempty" yaml:"excludedServers,omitempty"` +} + +// GetId returns the source ID. Implements catalog.SourceProperties interface. +func (s *McpSource) GetId() string { + return s.Id +} + +// GetProperties returns the source properties map. Implements catalog.SourceProperties interface. +func (s *McpSource) GetProperties() map[string]any { + return s.Properties +} + +// yamlMcpServer represents an MCP server in YAML format. +type yamlMcpServer struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Logo string `yaml:"logo,omitempty" json:"logo,omitempty"` + License string `yaml:"license,omitempty" json:"license,omitempty"` + LicenseLink string `yaml:"license_link,omitempty" json:"license_link,omitempty"` + Provider string `yaml:"provider,omitempty" json:"provider,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` + Transports []string `yaml:"transports,omitempty" json:"transports,omitempty"` + DocumentationUrl string `yaml:"documentationUrl,omitempty" json:"documentationUrl,omitempty"` + RepositoryUrl string `yaml:"repositoryUrl,omitempty" json:"repositoryUrl,omitempty"` + SourceCode string `yaml:"sourceCode,omitempty" json:"sourceCode,omitempty"` + Readme string `yaml:"readme,omitempty" json:"readme,omitempty"` + PublishedDate string `yaml:"publishedDate,omitempty" json:"publishedDate,omitempty"` + Tools []yamlMcpTool `yaml:"tools,omitempty" json:"tools,omitempty"` + Artifacts []yamlMcpArtifact `yaml:"artifacts,omitempty" json:"artifacts,omitempty"` + + // Deployment mode: "local" (default) or "remote" + DeploymentMode string `yaml:"deploymentMode,omitempty" json:"deploymentMode,omitempty"` + + // Endpoints for remote MCP servers (different URLs per transport) + Endpoints *yamlMcpEndpoints `yaml:"endpoints,omitempty" json:"endpoints,omitempty"` + + // CustomProperties following Model Registry pattern: + // - Tags are MetadataStringValue entries with empty string_value + // - Security indicators are MetadataBoolValue entries + CustomProperties map[string]yamlMetadataValue `yaml:"customProperties,omitempty" json:"customProperties,omitempty"` + + // Timestamps for database consistency + CreateTimeSinceEpoch string `yaml:"createTimeSinceEpoch,omitempty" json:"createTimeSinceEpoch,omitempty"` + LastUpdateTimeSinceEpoch string `yaml:"lastUpdateTimeSinceEpoch,omitempty" json:"lastUpdateTimeSinceEpoch,omitempty"` +} + +// yamlMcpTool represents an MCP tool in YAML format. +type yamlMcpTool struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + AccessType string `yaml:"accessType" json:"accessType"` + Parameters []yamlMcpToolParameter `yaml:"parameters,omitempty" json:"parameters,omitempty"` + Revoked bool `yaml:"revoked,omitempty" json:"revoked,omitempty"` + RevokedReason string `yaml:"revokedReason,omitempty" json:"revokedReason,omitempty"` +} + +// yamlMcpToolParameter represents a tool parameter in YAML format. +type yamlMcpToolParameter struct { + Name string `yaml:"name" json:"name"` + Type string `yaml:"type" json:"type"` + Description string `yaml:"description" json:"description"` + Required bool `yaml:"required" json:"required"` +} + +// yamlMetadataValue represents a metadata value in YAML format. +// It can be a string value, bool value, int value, or double value. +// Following Model Registry patterns: +// - Tags are MetadataStringValue with empty string_value +// - Security indicators are MetadataBoolValue +type yamlMetadataValue struct { + MetadataType string `yaml:"metadataType" json:"metadataType"` + StringValue *string `yaml:"string_value,omitempty" json:"string_value,omitempty"` + BoolValue *bool `yaml:"bool_value,omitempty" json:"bool_value,omitempty"` + IntValue *int64 `yaml:"int_value,omitempty" json:"int_value,omitempty"` + DoubleValue *float64 `yaml:"double_value,omitempty" json:"double_value,omitempty"` +} + +// yamlMcpArtifact represents an MCP server artifact (e.g., OCI image) in YAML format. +// Simplified format matching model artifacts: just uri + timestamps. +type yamlMcpArtifact struct { + Uri string `yaml:"uri" json:"uri"` + CreateTimeSinceEpoch string `yaml:"createTimeSinceEpoch,omitempty" json:"createTimeSinceEpoch,omitempty"` + LastUpdateTimeSinceEpoch string `yaml:"lastUpdateTimeSinceEpoch,omitempty" json:"lastUpdateTimeSinceEpoch,omitempty"` +} + +// yamlMcpEndpoints represents network endpoints for remote MCP servers. +type yamlMcpEndpoints struct { + Http string `yaml:"http,omitempty" json:"http,omitempty"` + Sse string `yaml:"sse,omitempty" json:"sse,omitempty"` +} + +// yamlMcpCatalog represents the YAML catalog structure. +type yamlMcpCatalog struct { + Source string `yaml:"source" json:"source"` + McpServers []yamlMcpServer `yaml:"mcp_servers" json:"mcp_servers"` +} + +// yamlMcpProvider provides MCP servers from a YAML file. +type yamlMcpProvider struct { + path string +} + +// ToMcpServerProviderRecord converts a YAML server to a provider record. +func (ys *yamlMcpServer) ToMcpServerProviderRecord() McpServerProviderRecord { + server := &dbmodels.McpServerImpl{} + + // Convert attributes + attrs := &dbmodels.McpServerAttributes{ + Name: &ys.Name, + } + + // Convert timestamps + if ys.CreateTimeSinceEpoch != "" { + if createTime, err := strconv.ParseInt(ys.CreateTimeSinceEpoch, 10, 64); err == nil { + attrs.CreateTimeSinceEpoch = &createTime + } + } + if ys.LastUpdateTimeSinceEpoch != "" { + if updateTime, err := strconv.ParseInt(ys.LastUpdateTimeSinceEpoch, 10, 64); err == nil { + attrs.LastUpdateTimeSinceEpoch = &updateTime + } + } + + server.Attributes = attrs + + // Convert properties + var properties []models.Properties + + if ys.Description != "" { + properties = append(properties, models.NewStringProperty("description", ys.Description, false)) + } + if ys.Logo != "" { + properties = append(properties, models.NewStringProperty("logo", ys.Logo, false)) + } + if ys.License != "" { + properties = append(properties, models.NewStringProperty("license", ys.License, false)) + } + if ys.LicenseLink != "" { + properties = append(properties, models.NewStringProperty("license_link", ys.LicenseLink, false)) + } + if ys.Provider != "" { + properties = append(properties, models.NewStringProperty("provider", ys.Provider, false)) + } + if ys.Version != "" { + properties = append(properties, models.NewStringProperty("version", ys.Version, false)) + } + // Store transports as JSON array + if len(ys.Transports) > 0 { + if transportsJSON, err := json.Marshal(ys.Transports); err == nil { + properties = append(properties, models.NewStringProperty("transports", string(transportsJSON), false)) + } + } + if ys.DocumentationUrl != "" { + properties = append(properties, models.NewStringProperty("documentationUrl", ys.DocumentationUrl, false)) + } + if ys.RepositoryUrl != "" { + properties = append(properties, models.NewStringProperty("repositoryUrl", ys.RepositoryUrl, false)) + } + if ys.SourceCode != "" { + properties = append(properties, models.NewStringProperty("sourceCode", ys.SourceCode, false)) + } + if ys.Readme != "" { + properties = append(properties, models.NewStringProperty("readme", ys.Readme, false)) + } + if ys.PublishedDate != "" { + properties = append(properties, models.NewStringProperty("publishedDate", ys.PublishedDate, false)) + } + + // Convert customProperties from YAML format to database properties + // Following Model Registry patterns: + // - Tags are MetadataStringValue entries with empty string_value + // - Security indicators are MetadataBoolValue entries + if len(ys.CustomProperties) > 0 { + // Extract tags (MetadataStringValue with empty string_value) + var tags []string + for key, val := range ys.CustomProperties { + if val.MetadataType == "MetadataStringValue" && val.StringValue != nil && *val.StringValue == "" { + tags = append(tags, key) + } + } + if len(tags) > 0 { + if tagsJSON, err := json.Marshal(tags); err == nil { + properties = append(properties, models.NewStringProperty("tags", string(tagsJSON), false)) + } + } + + // Extract security indicators (MetadataBoolValue entries) + for key, val := range ys.CustomProperties { + if val.MetadataType == "MetadataBoolValue" && val.BoolValue != nil { + properties = append(properties, models.NewBoolProperty(key, *val.BoolValue, false)) + } + } + } + + // Convert artifacts as JSON for storage (for local MCP servers with OCI images) + if len(ys.Artifacts) > 0 { + if artifactsJSON, err := json.Marshal(ys.Artifacts); err == nil { + properties = append(properties, models.NewStringProperty("artifacts", string(artifactsJSON), false)) + } + } + + // Convert deployment mode (default to "local" if not specified) + deploymentMode := ys.DeploymentMode + if deploymentMode == "" { + deploymentMode = "local" + } + properties = append(properties, models.NewStringProperty("deploymentMode", deploymentMode, false)) + + // Convert endpoints for remote servers + if ys.Endpoints != nil { + if endpointsJSON, err := json.Marshal(ys.Endpoints); err == nil { + properties = append(properties, models.NewStringProperty("endpoints", string(endpointsJSON), false)) + } + } + + // Convert tools as JSON for storage + if len(ys.Tools) > 0 { + if toolsJSON, err := json.Marshal(ys.Tools); err == nil { + properties = append(properties, models.NewStringProperty("tools", string(toolsJSON), false)) + } + } + + if len(properties) > 0 { + server.Properties = &properties + } + + // Convert tools to separate entities (for future use when tools are stored separately) + tools := make([]dbmodels.McpServerTool, 0, len(ys.Tools)) + for _, t := range ys.Tools { + tool := &dbmodels.McpServerToolImpl{ + Attributes: &dbmodels.McpServerToolAttributes{ + Name: &t.Name, + }, + } + + var toolProps []models.Properties + if t.Description != "" { + toolProps = append(toolProps, models.NewStringProperty("description", t.Description, false)) + } + if t.AccessType != "" { + toolProps = append(toolProps, models.NewStringProperty("accessType", t.AccessType, false)) + } + if len(t.Parameters) > 0 { + if paramsJSON, err := json.Marshal(t.Parameters); err == nil { + toolProps = append(toolProps, models.NewStringProperty("parameters", string(paramsJSON), false)) + } + } + // Always store revoked status (defaults to false) + toolProps = append(toolProps, models.NewBoolProperty("revoked", t.Revoked, false)) + if t.RevokedReason != "" { + toolProps = append(toolProps, models.NewStringProperty("revokedReason", t.RevokedReason, false)) + } + + if len(toolProps) > 0 { + tool.Properties = &toolProps + } + + tools = append(tools, tool) + } + + return McpServerProviderRecord{ + Server: server, + Tools: tools, + } +} + +// Models returns a channel of MCP server provider records. +func (p *yamlMcpProvider) Models(ctx context.Context) (<-chan McpServerProviderRecord, error) { + catalog, err := p.read() + if err != nil { + return nil, err + } + + ch := make(chan McpServerProviderRecord) + go func() { + defer close(ch) + p.emit(ctx, catalog, ch) + }() + + return ch, nil +} + +// read reads the YAML catalog file. +func (p *yamlMcpProvider) read() (*yamlMcpCatalog, error) { + buf, err := os.ReadFile(p.path) + if err != nil { + return nil, fmt.Errorf("failed to read %s file: %v", yamlCatalogPathKey, err) + } + + var catalog yamlMcpCatalog + if err = yaml.UnmarshalStrict(buf, &catalog); err != nil { + return nil, fmt.Errorf("failed to parse %s file: %v", yamlCatalogPathKey, err) + } + + return &catalog, nil +} + +// emit sends MCP server records to the output channel. +func (p *yamlMcpProvider) emit(ctx context.Context, catalog *yamlMcpCatalog, out chan<- McpServerProviderRecord) { + done := ctx.Done() + for _, server := range catalog.McpServers { + select { + case out <- server.ToMcpServerProviderRecord(): + case <-done: + return + } + } + + // Send an empty record to indicate that we're done with the batch. + select { + case out <- McpServerProviderRecord{}: + case <-done: + } +} + +// NewYamlMcpProvider creates a new YAML MCP provider. +// It detects the asset type from the YAML content and only processes files containing mcp_servers. +func NewYamlMcpProvider(ctx context.Context, source *McpSource, reldir string) (<-chan McpServerProviderRecord, error) { + // First, detect the asset type from the YAML content + assetType, err := common.DetectYamlAssetType(source, reldir) + if err != nil { + return nil, err + } + + // Only process this source if it contains MCP servers + if assetType != common.AssetTypeMcpServers { + glog.V(2).Infof("Skipping source %s in MCP provider: detected asset type is %s", source.Id, assetType) + // Return an empty channel that closes immediately + ch := make(chan McpServerProviderRecord) + close(ch) + return ch, nil + } + + p := &yamlMcpProvider{} + + path, exists := source.Properties[yamlCatalogPathKey].(string) + if !exists || path == "" { + return nil, fmt.Errorf("missing %s string property", yamlCatalogPathKey) + } + + if filepath.IsAbs(path) { + p.path = path + } else { + p.path = filepath.Join(reldir, path) + } + + glog.Infof("Loading MCP servers from YAML file: %s", p.path) + + return p.Models(ctx) +} + +// RegisteredMcpProviders holds the registered MCP provider functions. +var RegisteredMcpProviders = map[string]McpServerProviderFunc{ + "yaml": NewYamlMcpProvider, +} + +// RegisterMcpProvider registers an MCP provider function. +func RegisterMcpProvider(name string, callback McpServerProviderFunc) error { + if _, exists := RegisteredMcpProviders[name]; exists { + return fmt.Errorf("MCP provider type %s already exists", name) + } + RegisteredMcpProviders[name] = callback + return nil +} diff --git a/catalog/internal/server/openapi/.openapi-generator/FILES b/catalog/internal/server/openapi/.openapi-generator/FILES index ceebefcd5d..c8a5fefdda 100644 --- a/catalog/internal/server/openapi/.openapi-generator/FILES +++ b/catalog/internal/server/openapi/.openapi-generator/FILES @@ -1,4 +1,5 @@ api.go +api_mcp_catalog_service.go api_model_catalog_service.go error.go helpers.go @@ -11,6 +12,7 @@ model_base_resource_dates.go model_base_resource_list.go model_catalog_artifact.go model_catalog_artifact_list.go +model_catalog_asset_type.go model_catalog_label.go model_catalog_label_list.go model_catalog_metrics_artifact.go @@ -27,6 +29,16 @@ model_field_filter.go model_filter_option.go model_filter_option_range.go model_filter_options_list.go +model_mcp_artifact.go +model_mcp_deployment_mode.go +model_mcp_endpoints.go +model_mcp_security_indicator.go +model_mcp_server.go +model_mcp_server_list.go +model_mcp_tool.go +model_mcp_tool_access_type.go +model_mcp_tool_parameter.go +model_mcp_transport_type.go model_metadata_bool_value.go model_metadata_double_value.go model_metadata_int_value.go diff --git a/catalog/internal/server/openapi/api.go b/catalog/internal/server/openapi/api.go index 20e00c8341..88c877d1f9 100644 --- a/catalog/internal/server/openapi/api.go +++ b/catalog/internal/server/openapi/api.go @@ -18,6 +18,15 @@ import ( model "github.com/kubeflow/model-registry/catalog/pkg/openapi" ) +// McpCatalogServiceAPIRouter defines the required methods for binding the api requests to a responses for the McpCatalogServiceAPI +// The McpCatalogServiceAPIRouter implementation should parse necessary information from the http request, +// pass the data to a McpCatalogServiceAPIServicer to perform the required actions, then write the service results to the http response. +type McpCatalogServiceAPIRouter interface { + FindMcpServers(http.ResponseWriter, *http.Request) + FindMcpServersFilterOptions(http.ResponseWriter, *http.Request) + GetMcpServer(http.ResponseWriter, *http.Request) +} + // ModelCatalogServiceAPIRouter defines the required methods for binding the api requests to a responses for the ModelCatalogServiceAPI // The ModelCatalogServiceAPIRouter implementation should parse necessary information from the http request, // pass the data to a ModelCatalogServiceAPIServicer to perform the required actions, then write the service results to the http response. @@ -32,6 +41,16 @@ type ModelCatalogServiceAPIRouter interface { GetAllModelPerformanceArtifacts(http.ResponseWriter, *http.Request) } +// McpCatalogServiceAPIServicer defines the api actions for the McpCatalogServiceAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can be ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type McpCatalogServiceAPIServicer interface { + FindMcpServers(context.Context, string, string, string, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) + FindMcpServersFilterOptions(context.Context) (ImplResponse, error) + GetMcpServer(context.Context, string) (ImplResponse, error) +} + // ModelCatalogServiceAPIServicer defines the api actions for the ModelCatalogServiceAPI service // This interface intended to stay up to date with the openapi yaml used to generate it, // while the service implementation can be ignored with the .openapi-generator-ignore file @@ -40,7 +59,7 @@ type ModelCatalogServiceAPIServicer interface { FindLabels(context.Context, string, string, model.SortOrder, string) (ImplResponse, error) FindModels(context.Context, []string, string, []string, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) FindModelsFilterOptions(context.Context) (ImplResponse, error) - FindSources(context.Context, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) + FindSources(context.Context, string, model.CatalogAssetType, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) PreviewCatalogSource(context.Context, *os.File, string, string, string, *os.File) (ImplResponse, error) GetModel(context.Context, string, string) (ImplResponse, error) GetAllModelArtifacts(context.Context, string, string, []model.ArtifactTypeQueryParam, []model.ArtifactTypeQueryParam, string, string, string, model.SortOrder, string) (ImplResponse, error) diff --git a/catalog/internal/server/openapi/api_mcp_catalog_service.go b/catalog/internal/server/openapi/api_mcp_catalog_service.go new file mode 100644 index 0000000000..9b04026933 --- /dev/null +++ b/catalog/internal/server/openapi/api_mcp_catalog_service.go @@ -0,0 +1,200 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Model Catalog REST API + * + * REST API for Model Registry to create and manage ML model metadata + * + * API version: v1alpha1 + */ + +package openapi + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + + model "github.com/kubeflow/model-registry/catalog/pkg/openapi" +) + +// McpCatalogServiceAPIController binds http requests to an api service and writes the service results to the http response +type McpCatalogServiceAPIController struct { + service McpCatalogServiceAPIServicer + errorHandler ErrorHandler +} + +// McpCatalogServiceAPIOption for how the controller is set up. +type McpCatalogServiceAPIOption func(*McpCatalogServiceAPIController) + +// WithMcpCatalogServiceAPIErrorHandler inject ErrorHandler into controller +func WithMcpCatalogServiceAPIErrorHandler(h ErrorHandler) McpCatalogServiceAPIOption { + return func(c *McpCatalogServiceAPIController) { + c.errorHandler = h + } +} + +// NewMcpCatalogServiceAPIController creates a default api controller +func NewMcpCatalogServiceAPIController(s McpCatalogServiceAPIServicer, opts ...McpCatalogServiceAPIOption) *McpCatalogServiceAPIController { + controller := &McpCatalogServiceAPIController{ + service: s, + errorHandler: DefaultErrorHandler, + } + + for _, opt := range opts { + opt(controller) + } + + return controller +} + +// Routes returns all the api routes for the McpCatalogServiceAPIController +func (c *McpCatalogServiceAPIController) Routes() Routes { + return Routes{ + "FindMcpServers": Route{ + "FindMcpServers", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers", + c.FindMcpServers, + }, + "FindMcpServersFilterOptions": Route{ + "FindMcpServersFilterOptions", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers/filter_options", + c.FindMcpServersFilterOptions, + }, + "GetMcpServer": Route{ + "GetMcpServer", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers/{server_id}", + c.GetMcpServer, + }, + } +} + +// OrderedRoutes returns all the api routes in a deterministic order for the McpCatalogServiceAPIController +func (c *McpCatalogServiceAPIController) OrderedRoutes() []Route { + return []Route{ + Route{ + "FindMcpServers", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers", + c.FindMcpServers, + }, + Route{ + "FindMcpServersFilterOptions", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers/filter_options", + c.FindMcpServersFilterOptions, + }, + Route{ + "GetMcpServer", + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/mcp_servers/{server_id}", + c.GetMcpServer, + }, + } +} + +// FindMcpServers - List All McpServers +func (c *McpCatalogServiceAPIController) FindMcpServers(w http.ResponseWriter, r *http.Request) { + query, err := parseQuery(r.URL.RawQuery) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + var nameParam string + if query.Has("name") { + param := query.Get("name") + + nameParam = param + } else { + } + var qParam string + if query.Has("q") { + param := query.Get("q") + + qParam = param + } else { + } + var filterQueryParam string + if query.Has("filterQuery") { + param := query.Get("filterQuery") + + filterQueryParam = param + } else { + } + var namedQueryParam string + if query.Has("namedQuery") { + param := query.Get("namedQuery") + + namedQueryParam = param + } else { + } + var pageSizeParam string + if query.Has("pageSize") { + param := query.Get("pageSize") + + pageSizeParam = param + } else { + } + var orderByParam model.OrderByField + if query.Has("orderBy") { + param := model.OrderByField(query.Get("orderBy")) + + orderByParam = param + } else { + } + var sortOrderParam model.SortOrder + if query.Has("sortOrder") { + param := model.SortOrder(query.Get("sortOrder")) + + sortOrderParam = param + } else { + } + var nextPageTokenParam string + if query.Has("nextPageToken") { + param := query.Get("nextPageToken") + + nextPageTokenParam = param + } else { + } + result, err := c.service.FindMcpServers(r.Context(), nameParam, qParam, filterQueryParam, namedQueryParam, pageSizeParam, orderByParam, sortOrderParam, nextPageTokenParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + _ = EncodeJSONResponse(result.Body, &result.Code, w) +} + +// FindMcpServersFilterOptions - Lists fields and available options that can be used in `filterQuery` on the list MCP servers endpoint. +func (c *McpCatalogServiceAPIController) FindMcpServersFilterOptions(w http.ResponseWriter, r *http.Request) { + result, err := c.service.FindMcpServersFilterOptions(r.Context()) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + _ = EncodeJSONResponse(result.Body, &result.Code, w) +} + +// GetMcpServer - Get an McpServer +func (c *McpCatalogServiceAPIController) GetMcpServer(w http.ResponseWriter, r *http.Request) { + serverIdParam := chi.URLParam(r, "server_id") + if serverIdParam == "" { + c.errorHandler(w, r, &RequiredError{"server_id"}, nil) + return + } + result, err := c.service.GetMcpServer(r.Context(), serverIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + _ = EncodeJSONResponse(result.Body, &result.Code, w) +} diff --git a/catalog/internal/server/openapi/api_mcp_catalog_service_service.go b/catalog/internal/server/openapi/api_mcp_catalog_service_service.go new file mode 100644 index 0000000000..0a252d360a --- /dev/null +++ b/catalog/internal/server/openapi/api_mcp_catalog_service_service.go @@ -0,0 +1,187 @@ +package openapi + +import ( + "context" + "errors" + "math" + "net/http" + "strconv" + "strings" + + model "github.com/kubeflow/model-registry/catalog/pkg/openapi" +) + +// McpCatalogProvider defines the interface for MCP catalog data providers. +// This allows switching between embedded data and database-backed implementations. +type McpCatalogProvider interface { + ListMcpServers(ctx context.Context, name string, q string, filterQuery string, namedQuery string) ([]model.McpServer, error) + GetMcpServer(ctx context.Context, serverId string) (*model.McpServer, error) + GetFilterOptions(ctx context.Context) (*model.FilterOptionsList, error) +} + +// McpCatalogServiceAPIService implements the McpCatalogServiceAPIServicer interface +type McpCatalogServiceAPIService struct { + provider McpCatalogProvider +} + +var _ McpCatalogServiceAPIServicer = &McpCatalogServiceAPIService{} + +// NewMcpCatalogServiceAPIService creates a new MCP catalog service +func NewMcpCatalogServiceAPIService(provider McpCatalogProvider) McpCatalogServiceAPIServicer { + return &McpCatalogServiceAPIService{ + provider: provider, + } +} + +// FindMcpServers - List All McpServers +func (s *McpCatalogServiceAPIService) FindMcpServers(ctx context.Context, name string, q string, filterQuery string, namedQuery string, strPageSize string, orderBy model.OrderByField, sortOrder model.SortOrder, nextPageToken string) (ImplResponse, error) { + servers, err := s.provider.ListMcpServers(ctx, name, q, filterQuery, namedQuery) + if err != nil { + return ErrorResponse(http.StatusInternalServerError, err), err + } + + if len(servers) > math.MaxInt32 { + err := errors.New("too many MCP servers") + return ErrorResponse(http.StatusInternalServerError, err), err + } + + paginator := newMcpPaginator(strPageSize, nextPageToken) + + // Sort servers + cmpFunc, err := genMcpServerCmpFunc(orderBy, sortOrder) + if err != nil { + return ErrorResponse(http.StatusBadRequest, err), err + } + + // Create a copy for sorting + sortedServers := make([]model.McpServer, len(servers)) + copy(sortedServers, servers) + + // Sort using stable sort + mcpSortStable(sortedServers, cmpFunc) + + // Paginate + pagedItems, nextToken := paginator.paginate(sortedServers) + + res := model.McpServerList{ + PageSize: paginator.pageSize, + Items: pagedItems, + Size: int32(len(pagedItems)), + NextPageToken: nextToken, + } + + return Response(http.StatusOK, res), nil +} + +// GetMcpServer - Get an McpServer +func (s *McpCatalogServiceAPIService) GetMcpServer(ctx context.Context, serverId string) (ImplResponse, error) { + server, err := s.provider.GetMcpServer(ctx, serverId) + if err != nil { + return ErrorResponse(http.StatusInternalServerError, err), err + } + + if server == nil { + return notFound("MCP server not found"), nil + } + + return Response(http.StatusOK, server), nil +} + +// FindMcpServersFilterOptions - Lists fields and available options that can be used in `filterQuery` on the list MCP servers endpoint. +func (s *McpCatalogServiceAPIService) FindMcpServersFilterOptions(ctx context.Context) (ImplResponse, error) { + filterOptions, err := s.provider.GetFilterOptions(ctx) + if err != nil { + return ErrorResponse(http.StatusInternalServerError, err), err + } + + return Response(http.StatusOK, filterOptions), nil +} + +// genMcpServerCmpFunc generates a comparison function for sorting MCP servers +func genMcpServerCmpFunc(orderBy model.OrderByField, sortOrder model.SortOrder) (func(model.McpServer, model.McpServer) int, error) { + multiplier := 1 + switch model.SortOrder(strings.ToUpper(string(sortOrder))) { + case model.SORTORDER_DESC: + multiplier = -1 + case model.SORTORDER_ASC, "": + multiplier = 1 + default: + return nil, errors.New("unsupported sort order") + } + + switch model.OrderByField(strings.ToUpper(string(orderBy))) { + case model.ORDERBYFIELD_ID, "": + return func(a, b model.McpServer) int { + return multiplier * strings.Compare(a.Id, b.Id) + }, nil + case model.ORDERBYFIELD_NAME: + return func(a, b model.McpServer) int { + return multiplier * strings.Compare(a.Name, b.Name) + }, nil + default: + return nil, errors.New("unsupported order by field for MCP servers") + } +} + +// mcpSortStable is a stable sort implementation for MCP servers +func mcpSortStable(items []model.McpServer, cmp func(model.McpServer, model.McpServer) int) { + // Use insertion sort for stable sorting + for i := 1; i < len(items); i++ { + for j := i; j > 0 && cmp(items[j-1], items[j]) > 0; j-- { + items[j-1], items[j] = items[j], items[j-1] + } + } +} + +// mcpServerPaginator handles pagination for MCP servers +type mcpServerPaginator struct { + pageSize int32 + offset int32 +} + +// newMcpPaginator creates a paginator for MCP servers +func newMcpPaginator(strPageSize string, nextPageToken string) *mcpServerPaginator { + pageSize := int32(10) + if strPageSize != "" { + parsed, err := strconv.ParseInt(strPageSize, 10, 32) + if err == nil && parsed > 0 { + pageSize = int32(parsed) + } + } + + offset := int32(0) + if nextPageToken != "" { + // Simple offset-based pagination + parsed, err := strconv.ParseInt(nextPageToken, 10, 32) + if err == nil { + offset = int32(parsed) + } + } + + return &mcpServerPaginator{ + pageSize: pageSize, + offset: offset, + } +} + +// paginate returns a page of items and the next page token +func (p *mcpServerPaginator) paginate(items []model.McpServer) ([]model.McpServer, string) { + start := int(p.offset) + if start >= len(items) { + return []model.McpServer{}, "" + } + + end := start + int(p.pageSize) + if end > len(items) { + end = len(items) + } + + result := items[start:end] + + var next string + if end < len(items) { + next = strconv.Itoa(end) + } + + return result, next +} diff --git a/catalog/internal/server/openapi/api_model_catalog_service.go b/catalog/internal/server/openapi/api_model_catalog_service.go index 38a2a0171d..75502fbd27 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service.go +++ b/catalog/internal/server/openapi/api_model_catalog_service.go @@ -296,6 +296,13 @@ func (c *ModelCatalogServiceAPIController) FindSources(w http.ResponseWriter, r nameParam = param } else { } + var assetTypeParam model.CatalogAssetType + if query.Has("assetType") { + param := model.CatalogAssetType(query.Get("assetType")) + + assetTypeParam = param + } else { + } var pageSizeParam string if query.Has("pageSize") { param := query.Get("pageSize") @@ -324,7 +331,7 @@ func (c *ModelCatalogServiceAPIController) FindSources(w http.ResponseWriter, r nextPageTokenParam = param } else { } - result, err := c.service.FindSources(r.Context(), nameParam, pageSizeParam, orderByParam, sortOrderParam, nextPageTokenParam) + result, err := c.service.FindSources(r.Context(), nameParam, assetTypeParam, pageSizeParam, orderByParam, sortOrderParam, nextPageTokenParam) // If an error occurred, encode the error with the status code if err != nil { c.errorHandler(w, r, err, &result) diff --git a/catalog/internal/server/openapi/api_model_catalog_service_service.go b/catalog/internal/server/openapi/api_model_catalog_service_service.go index 891bde11e6..40d925f814 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service_service.go +++ b/catalog/internal/server/openapi/api_model_catalog_service_service.go @@ -278,7 +278,7 @@ func (m *ModelCatalogServiceAPIService) GetModel(ctx context.Context, sourceID, return Response(http.StatusOK, model), nil } -func (m *ModelCatalogServiceAPIService) FindSources(ctx context.Context, name string, strPageSize string, orderBy model.OrderByField, sortOrder model.SortOrder, nextPageToken string) (ImplResponse, error) { +func (m *ModelCatalogServiceAPIService) FindSources(ctx context.Context, name string, assetType model.CatalogAssetType, strPageSize string, orderBy model.OrderByField, sortOrder model.SortOrder, nextPageToken string) (ImplResponse, error) { sources := m.sources.All() if len(sources) > math.MaxInt32 { err := errors.New("too many registered models") @@ -310,6 +310,18 @@ func (m *ModelCatalogServiceAPIService) FindSources(ctx context.Context, name st continue } + // Filter by asset type if specified + if assetType != "" { + // Get the source's asset type, defaulting to "models" if not set + sourceAssetType := model.CATALOGASSETTYPE_MODELS + if v.AssetType != nil { + sourceAssetType = *v.AssetType + } + if sourceAssetType != assetType { + continue + } + } + // Merge status from database if available if statuses != nil { if status, ok := statuses[v.Id]; ok { diff --git a/catalog/internal/server/openapi/api_model_catalog_service_service_test.go b/catalog/internal/server/openapi/api_model_catalog_service_service_test.go index 205292bf13..0a661c0fff 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service_service_test.go +++ b/catalog/internal/server/openapi/api_model_catalog_service_service_test.go @@ -745,6 +745,7 @@ func TestFindSources(t *testing.T) { resp, err := service.FindSources( context.Background(), tc.nameFilter, + "", // assetType - empty means no filter tc.pageSize, tc.orderBy, tc.sortOrder, diff --git a/catalog/internal/server/openapi/type_asserts.go b/catalog/internal/server/openapi/type_asserts.go index 061eeabe67..ead32c9dd2 100644 --- a/catalog/internal/server/openapi/type_asserts.go +++ b/catalog/internal/server/openapi/type_asserts.go @@ -109,6 +109,16 @@ func AssertCatalogArtifactListRequired(obj model.CatalogArtifactList) error { return nil } +// AssertCatalogAssetTypeConstraints checks if the values respects the defined constraints +func AssertCatalogAssetTypeConstraints(obj model.CatalogAssetType) error { + return nil +} + +// AssertCatalogAssetTypeRequired checks if the required fields are not zero-ed +func AssertCatalogAssetTypeRequired(obj model.CatalogAssetType) error { + return nil +} + // AssertCatalogLabelConstraints checks if the values respects the defined constraints func AssertCatalogLabelConstraints(obj model.CatalogLabel) error { return nil @@ -424,6 +434,218 @@ func AssertFilterOptionsListRequired(obj model.FilterOptionsList) error { return nil } +// AssertMcpArtifactConstraints checks if the values respects the defined constraints +func AssertMcpArtifactConstraints(obj model.McpArtifact) error { + return nil +} + +// AssertMcpArtifactRequired checks if the required fields are not zero-ed +func AssertMcpArtifactRequired(obj model.McpArtifact) error { + elements := map[string]interface{}{ + "uri": obj.Uri, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + return nil +} + +// AssertMcpDeploymentModeConstraints checks if the values respects the defined constraints +func AssertMcpDeploymentModeConstraints(obj model.McpDeploymentMode) error { + return nil +} + +// AssertMcpDeploymentModeRequired checks if the required fields are not zero-ed +func AssertMcpDeploymentModeRequired(obj model.McpDeploymentMode) error { + return nil +} + +// AssertMcpEndpointsConstraints checks if the values respects the defined constraints +func AssertMcpEndpointsConstraints(obj model.McpEndpoints) error { + return nil +} + +// AssertMcpEndpointsRequired checks if the required fields are not zero-ed +func AssertMcpEndpointsRequired(obj model.McpEndpoints) error { + return nil +} + +// AssertMcpSecurityIndicatorConstraints checks if the values respects the defined constraints +func AssertMcpSecurityIndicatorConstraints(obj model.McpSecurityIndicator) error { + return nil +} + +// AssertMcpSecurityIndicatorRequired checks if the required fields are not zero-ed +func AssertMcpSecurityIndicatorRequired(obj model.McpSecurityIndicator) error { + return nil +} + +// AssertMcpServerConstraints checks if the values respects the defined constraints +func AssertMcpServerConstraints(obj model.McpServer) error { + for _, el := range obj.Tools { + if err := AssertMcpToolConstraints(el); err != nil { + return err + } + } + if obj.SecurityIndicators != nil { + if err := AssertMcpSecurityIndicatorConstraints(*obj.SecurityIndicators); err != nil { + return err + } + } + for _, el := range obj.Artifacts { + if err := AssertMcpArtifactConstraints(el); err != nil { + return err + } + } + if obj.Endpoints != nil { + if err := AssertMcpEndpointsConstraints(*obj.Endpoints); err != nil { + return err + } + } + return nil +} + +// AssertMcpServerListConstraints checks if the values respects the defined constraints +func AssertMcpServerListConstraints(obj model.McpServerList) error { + for _, el := range obj.Items { + if err := AssertMcpServerConstraints(el); err != nil { + return err + } + } + return nil +} + +// AssertMcpServerListRequired checks if the required fields are not zero-ed +func AssertMcpServerListRequired(obj model.McpServerList) error { + elements := map[string]interface{}{ + "nextPageToken": obj.NextPageToken, + "pageSize": obj.PageSize, + "size": obj.Size, + "items": obj.Items, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + for _, el := range obj.Items { + if err := AssertMcpServerRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertMcpServerRequired checks if the required fields are not zero-ed +func AssertMcpServerRequired(obj model.McpServer) error { + elements := map[string]interface{}{ + "id": obj.Id, + "name": obj.Name, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + for _, el := range obj.Tools { + if err := AssertMcpToolRequired(el); err != nil { + return err + } + } + if obj.SecurityIndicators != nil { + if err := AssertMcpSecurityIndicatorRequired(*obj.SecurityIndicators); err != nil { + return err + } + } + for _, el := range obj.Artifacts { + if err := AssertMcpArtifactRequired(el); err != nil { + return err + } + } + if obj.Endpoints != nil { + if err := AssertMcpEndpointsRequired(*obj.Endpoints); err != nil { + return err + } + } + return nil +} + +// AssertMcpToolAccessTypeConstraints checks if the values respects the defined constraints +func AssertMcpToolAccessTypeConstraints(obj model.McpToolAccessType) error { + return nil +} + +// AssertMcpToolAccessTypeRequired checks if the required fields are not zero-ed +func AssertMcpToolAccessTypeRequired(obj model.McpToolAccessType) error { + return nil +} + +// AssertMcpToolConstraints checks if the values respects the defined constraints +func AssertMcpToolConstraints(obj model.McpTool) error { + for _, el := range obj.Parameters { + if err := AssertMcpToolParameterConstraints(el); err != nil { + return err + } + } + return nil +} + +// AssertMcpToolParameterConstraints checks if the values respects the defined constraints +func AssertMcpToolParameterConstraints(obj model.McpToolParameter) error { + return nil +} + +// AssertMcpToolParameterRequired checks if the required fields are not zero-ed +func AssertMcpToolParameterRequired(obj model.McpToolParameter) error { + elements := map[string]interface{}{ + "name": obj.Name, + "type": obj.Type, + "required": obj.Required, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + return nil +} + +// AssertMcpToolRequired checks if the required fields are not zero-ed +func AssertMcpToolRequired(obj model.McpTool) error { + elements := map[string]interface{}{ + "name": obj.Name, + "accessType": obj.AccessType, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + for _, el := range obj.Parameters { + if err := AssertMcpToolParameterRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertMcpTransportTypeConstraints checks if the values respects the defined constraints +func AssertMcpTransportTypeConstraints(obj model.McpTransportType) error { + return nil +} + +// AssertMcpTransportTypeRequired checks if the required fields are not zero-ed +func AssertMcpTransportTypeRequired(obj model.McpTransportType) error { + return nil +} + // AssertMetadataBoolValueConstraints checks if the values respects the defined constraints func AssertMetadataBoolValueConstraints(obj model.MetadataBoolValue) error { return nil diff --git a/catalog/pkg/openapi/.openapi-generator/FILES b/catalog/pkg/openapi/.openapi-generator/FILES index 7e1775534c..de5a46e298 100644 --- a/catalog/pkg/openapi/.openapi-generator/FILES +++ b/catalog/pkg/openapi/.openapi-generator/FILES @@ -1,3 +1,4 @@ +api_mcp_catalog_service.go api_model_catalog_service.go client.go configuration.go @@ -8,6 +9,7 @@ model_base_resource_dates.go model_base_resource_list.go model_catalog_artifact.go model_catalog_artifact_list.go +model_catalog_asset_type.go model_catalog_label.go model_catalog_label_list.go model_catalog_metrics_artifact.go @@ -24,6 +26,16 @@ model_field_filter.go model_filter_option.go model_filter_option_range.go model_filter_options_list.go +model_mcp_artifact.go +model_mcp_deployment_mode.go +model_mcp_endpoints.go +model_mcp_security_indicator.go +model_mcp_server.go +model_mcp_server_list.go +model_mcp_tool.go +model_mcp_tool_access_type.go +model_mcp_tool_parameter.go +model_mcp_transport_type.go model_metadata_bool_value.go model_metadata_double_value.go model_metadata_int_value.go diff --git a/catalog/pkg/openapi/api_mcp_catalog_service.go b/catalog/pkg/openapi/api_mcp_catalog_service.go new file mode 100644 index 0000000000..138eb79389 --- /dev/null +++ b/catalog/pkg/openapi/api_mcp_catalog_service.go @@ -0,0 +1,501 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + +// McpCatalogServiceAPIService McpCatalogServiceAPI service +type McpCatalogServiceAPIService service + +type ApiFindMcpServersRequest struct { + ctx context.Context + ApiService *McpCatalogServiceAPIService + name *string + q *string + filterQuery *string + namedQuery *string + pageSize *string + orderBy *OrderByField + sortOrder *SortOrder + nextPageToken *string +} + +// Name of entity to search. +func (r ApiFindMcpServersRequest) Name(name string) ApiFindMcpServersRequest { + r.name = &name + return r +} + +// Free-form keyword search used to filter MCP servers by name, description, or provider. +func (r ApiFindMcpServersRequest) Q(q string) ApiFindMcpServersRequest { + r.q = &q + return r +} + +// A SQL-like query string to filter the list of entities. The query supports rich filtering capabilities with automatic type inference. **Supported Operators:** - Comparison: `=`, `!=`, `<>`, `>`, `<`, `>=`, `<=` - Pattern matching: `LIKE`, `ILIKE` (case-insensitive) - Set membership: `IN` - Logical: `AND`, `OR` - Grouping: `()` for complex expressions **Data Types:** - Strings: `\"value\"` or `'value'` - Numbers: `42`, `3.14`, `1e-5` - Booleans: `true`, `false` (case-insensitive) **Property Access:** - Standard properties: `name`, `id`, `state`, `createTimeSinceEpoch` - Custom properties: Any user-defined property name - Escaped properties: Use backticks for special characters: `` `custom-property` `` - Type-specific access: `property.string_value`, `property.double_value`, `property.int_value`, `property.bool_value` **Examples:** - Basic: `name = \"my-model\"` - Comparison: `accuracy > 0.95` - Pattern: `name LIKE \"%tensorflow%\"` - Complex: `(name = \"model-a\" OR name = \"model-b\") AND state = \"LIVE\"` - Custom property: `framework.string_value = \"pytorch\"` - Escaped property: `` `mlflow.source.type` = \"notebook\" `` +func (r ApiFindMcpServersRequest) FilterQuery(filterQuery string) ApiFindMcpServersRequest { + r.filterQuery = &filterQuery + return r +} + +// Apply a pre-defined named query to filter the list of entities. Named queries are configured in the catalog sources YAML file and provide reusable filter presets for common filtering scenarios. **Configuration Example:** ```yaml namedQueries: production_ready: provider: operator: \"IN\" value: [\"Dynatrace\", \"CNCF\", \"GitHub\"] verifiedSource: operator: \"=\" value: true monitoring_tools: deploymentMode: operator: \"=\" value: \"local\" ``` **Usage:** - Apply single named query: `?namedQuery=production_ready` - Named queries work alongside other filters (filterQuery, name) - If the named query doesn't exist, a 400 Bad Request is returned **Behavior:** - Named query filters are applied in addition to any other filters - Named queries can reference customProperties and standard fields - Results must satisfy ALL conditions in the named query +func (r ApiFindMcpServersRequest) NamedQuery(namedQuery string) ApiFindMcpServersRequest { + r.namedQuery = &namedQuery + return r +} + +// Number of entities in each page. +func (r ApiFindMcpServersRequest) PageSize(pageSize string) ApiFindMcpServersRequest { + r.pageSize = &pageSize + return r +} + +// Specifies the order by criteria for listing entities. +func (r ApiFindMcpServersRequest) OrderBy(orderBy OrderByField) ApiFindMcpServersRequest { + r.orderBy = &orderBy + return r +} + +// Specifies the sort order for listing entities, defaults to ASC. +func (r ApiFindMcpServersRequest) SortOrder(sortOrder SortOrder) ApiFindMcpServersRequest { + r.sortOrder = &sortOrder + return r +} + +// Token to use to retrieve next page of results. +func (r ApiFindMcpServersRequest) NextPageToken(nextPageToken string) ApiFindMcpServersRequest { + r.nextPageToken = &nextPageToken + return r +} + +func (r ApiFindMcpServersRequest) Execute() (*McpServerList, *http.Response, error) { + return r.ApiService.FindMcpServersExecute(r) +} + +/* +FindMcpServers List All McpServers + +Gets a list of all `McpServer` entities. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiFindMcpServersRequest +*/ +func (a *McpCatalogServiceAPIService) FindMcpServers(ctx context.Context) ApiFindMcpServersRequest { + return ApiFindMcpServersRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return McpServerList +func (a *McpCatalogServiceAPIService) FindMcpServersExecute(r ApiFindMcpServersRequest) (*McpServerList, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *McpServerList + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "McpCatalogServiceAPIService.FindMcpServers") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/model_catalog/v1alpha1/mcp_servers" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.name != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "name", r.name, "form", "") + } + if r.q != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "q", r.q, "form", "") + } + if r.filterQuery != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "filterQuery", r.filterQuery, "form", "") + } + if r.namedQuery != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "namedQuery", r.namedQuery, "form", "") + } + if r.pageSize != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "pageSize", r.pageSize, "form", "") + } + if r.orderBy != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "orderBy", r.orderBy, "form", "") + } + if r.sortOrder != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "sortOrder", r.sortOrder, "form", "") + } + if r.nextPageToken != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "nextPageToken", r.nextPageToken, "form", "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiFindMcpServersFilterOptionsRequest struct { + ctx context.Context + ApiService *McpCatalogServiceAPIService +} + +func (r ApiFindMcpServersFilterOptionsRequest) Execute() (*FilterOptionsList, *http.Response, error) { + return r.ApiService.FindMcpServersFilterOptionsExecute(r) +} + +/* +FindMcpServersFilterOptions Lists fields and available options that can be used in `filterQuery` on the list MCP servers endpoint. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiFindMcpServersFilterOptionsRequest +*/ +func (a *McpCatalogServiceAPIService) FindMcpServersFilterOptions(ctx context.Context) ApiFindMcpServersFilterOptionsRequest { + return ApiFindMcpServersFilterOptionsRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return FilterOptionsList +func (a *McpCatalogServiceAPIService) FindMcpServersFilterOptionsExecute(r ApiFindMcpServersFilterOptionsRequest) (*FilterOptionsList, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *FilterOptionsList + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "McpCatalogServiceAPIService.FindMcpServersFilterOptions") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/model_catalog/v1alpha1/mcp_servers/filter_options" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetMcpServerRequest struct { + ctx context.Context + ApiService *McpCatalogServiceAPIService + serverId string +} + +func (r ApiGetMcpServerRequest) Execute() (*McpServer, *http.Response, error) { + return r.ApiService.GetMcpServerExecute(r) +} + +/* +GetMcpServer Get an McpServer + +Gets a single `McpServer` entity by ID. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param serverId A unique identifier for an `McpServer`. + @return ApiGetMcpServerRequest +*/ +func (a *McpCatalogServiceAPIService) GetMcpServer(ctx context.Context, serverId string) ApiGetMcpServerRequest { + return ApiGetMcpServerRequest{ + ApiService: a, + ctx: ctx, + serverId: serverId, + } +} + +// Execute executes the request +// +// @return McpServer +func (a *McpCatalogServiceAPIService) GetMcpServerExecute(r ApiGetMcpServerRequest) (*McpServer, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *McpServer + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "McpCatalogServiceAPIService.GetMcpServer") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/model_catalog/v1alpha1/mcp_servers/{server_id}" + localVarPath = strings.Replace(localVarPath, "{"+"server_id"+"}", url.PathEscape(parameterValueToString(r.serverId, "serverId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/catalog/pkg/openapi/api_model_catalog_service.go b/catalog/pkg/openapi/api_model_catalog_service.go index 7c9b8d4abe..1bcf156231 100644 --- a/catalog/pkg/openapi/api_model_catalog_service.go +++ b/catalog/pkg/openapi/api_model_catalog_service.go @@ -578,6 +578,7 @@ type ApiFindSourcesRequest struct { ctx context.Context ApiService *ModelCatalogServiceAPIService name *string + assetType *CatalogAssetType pageSize *string orderBy *OrderByField sortOrder *SortOrder @@ -590,6 +591,12 @@ func (r ApiFindSourcesRequest) Name(name string) ApiFindSourcesRequest { return r } +// Filter sources by asset type. - `models`: Sources containing AI/ML models - `mcp_servers`: Sources containing MCP (Model Context Protocol) servers +func (r ApiFindSourcesRequest) AssetType(assetType CatalogAssetType) ApiFindSourcesRequest { + r.assetType = &assetType + return r +} + // Number of entities in each page. func (r ApiFindSourcesRequest) PageSize(pageSize string) ApiFindSourcesRequest { r.pageSize = &pageSize @@ -658,6 +665,13 @@ func (a *ModelCatalogServiceAPIService) FindSourcesExecute(r ApiFindSourcesReque if r.name != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "name", r.name, "form", "") } + if r.assetType != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "assetType", r.assetType, "form", "") + } else { + var defaultValue CatalogAssetType = "models" + parameterAddToHeaderOrQuery(localVarQueryParams, "assetType", defaultValue, "form", "") + r.assetType = &defaultValue + } if r.pageSize != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "pageSize", r.pageSize, "form", "") } diff --git a/catalog/pkg/openapi/client.go b/catalog/pkg/openapi/client.go index e730c7efa7..885fd9b71b 100644 --- a/catalog/pkg/openapi/client.go +++ b/catalog/pkg/openapi/client.go @@ -48,6 +48,8 @@ type APIClient struct { // API Services + McpCatalogServiceAPI *McpCatalogServiceAPIService + ModelCatalogServiceAPI *ModelCatalogServiceAPIService } @@ -67,6 +69,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services + c.McpCatalogServiceAPI = (*McpCatalogServiceAPIService)(&c.common) c.ModelCatalogServiceAPI = (*ModelCatalogServiceAPIService)(&c.common) return c diff --git a/catalog/pkg/openapi/model_catalog_asset_type.go b/catalog/pkg/openapi/model_catalog_asset_type.go new file mode 100644 index 0000000000..097716ee55 --- /dev/null +++ b/catalog/pkg/openapi/model_catalog_asset_type.go @@ -0,0 +1,110 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "fmt" +) + +// CatalogAssetType The type of assets managed by a catalog source. - `models`: AI/ML models (default) - `mcp_servers`: MCP (Model Context Protocol) servers +type CatalogAssetType string + +// List of CatalogAssetType +const ( + CATALOGASSETTYPE_MODELS CatalogAssetType = "models" + CATALOGASSETTYPE_MCP_SERVERS CatalogAssetType = "mcp_servers" +) + +// All allowed values of CatalogAssetType enum +var AllowedCatalogAssetTypeEnumValues = []CatalogAssetType{ + "models", + "mcp_servers", +} + +func (v *CatalogAssetType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := CatalogAssetType(value) + for _, existing := range AllowedCatalogAssetTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid CatalogAssetType", value) +} + +// NewCatalogAssetTypeFromValue returns a pointer to a valid CatalogAssetType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewCatalogAssetTypeFromValue(v string) (*CatalogAssetType, error) { + ev := CatalogAssetType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for CatalogAssetType: valid values are %v", v, AllowedCatalogAssetTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v CatalogAssetType) IsValid() bool { + for _, existing := range AllowedCatalogAssetTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to CatalogAssetType value +func (v CatalogAssetType) Ptr() *CatalogAssetType { + return &v +} + +type NullableCatalogAssetType struct { + value *CatalogAssetType + isSet bool +} + +func (v NullableCatalogAssetType) Get() *CatalogAssetType { + return v.value +} + +func (v *NullableCatalogAssetType) Set(val *CatalogAssetType) { + v.value = val + v.isSet = true +} + +func (v NullableCatalogAssetType) IsSet() bool { + return v.isSet +} + +func (v *NullableCatalogAssetType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCatalogAssetType(val *CatalogAssetType) *NullableCatalogAssetType { + return &NullableCatalogAssetType{value: val, isSet: true} +} + +func (v NullableCatalogAssetType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCatalogAssetType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_catalog_source.go b/catalog/pkg/openapi/model_catalog_source.go index 6357be1ec6..e310f29966 100644 --- a/catalog/pkg/openapi/model_catalog_source.go +++ b/catalog/pkg/openapi/model_catalog_source.go @@ -26,8 +26,9 @@ type CatalogSource struct { // Whether the catalog source is enabled. Enabled *bool `json:"enabled,omitempty"` // Labels for the catalog source. - Labels []string `json:"labels"` - Status *CatalogSourceStatus `json:"status,omitempty"` + Labels []string `json:"labels"` + AssetType *CatalogAssetType `json:"assetType,omitempty"` + Status *CatalogSourceStatus `json:"status,omitempty"` // Detailed error information when the status is \"Error\". This field is null or empty when the source is functioning normally. Error NullableString `json:"error,omitempty"` // Optional list of glob patterns for models to include. If specified, only models matching at least one pattern will be included. If omitted, all models are considered for inclusion. Pattern Syntax: - Only the `*` wildcard is supported (matches zero or more characters) - Patterns are case-insensitive (e.g., `Granite/_*` matches `granite/model` and `GRANITE/model`) - Patterns match the entire model name (anchored at start and end) - Wildcards can appear anywhere: `Granite/_*`, `*-beta`, `*deprecated*`, `*_/old*` Examples: - `ibm-granite/_*` - matches all models starting with \"ibm-granite/\" - `meta-llama/_*` - matches all models in the meta-llama namespace - `*` - matches all models Constraints: - Patterns cannot be empty or whitespace-only - A pattern cannot appear in both includedModels and excludedModels @@ -49,6 +50,8 @@ func NewCatalogSource(id string, name string, labels []string) *CatalogSource { var enabled bool = true this.Enabled = &enabled this.Labels = labels + var assetType CatalogAssetType = CATALOGASSETTYPE_MODELS + this.AssetType = &assetType return &this } @@ -59,6 +62,8 @@ func NewCatalogSourceWithDefaults() *CatalogSource { this := CatalogSource{} var enabled bool = true this.Enabled = &enabled + var assetType CatalogAssetType = CATALOGASSETTYPE_MODELS + this.AssetType = &assetType return &this } @@ -166,6 +171,38 @@ func (o *CatalogSource) SetLabels(v []string) { o.Labels = v } +// GetAssetType returns the AssetType field value if set, zero value otherwise. +func (o *CatalogSource) GetAssetType() CatalogAssetType { + if o == nil || IsNil(o.AssetType) { + var ret CatalogAssetType + return ret + } + return *o.AssetType +} + +// GetAssetTypeOk returns a tuple with the AssetType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CatalogSource) GetAssetTypeOk() (*CatalogAssetType, bool) { + if o == nil || IsNil(o.AssetType) { + return nil, false + } + return o.AssetType, true +} + +// HasAssetType returns a boolean if a field has been set. +func (o *CatalogSource) HasAssetType() bool { + if o != nil && !IsNil(o.AssetType) { + return true + } + + return false +} + +// SetAssetType gets a reference to the given CatalogAssetType and assigns it to the AssetType field. +func (o *CatalogSource) SetAssetType(v CatalogAssetType) { + o.AssetType = &v +} + // GetStatus returns the Status field value if set, zero value otherwise. func (o *CatalogSource) GetStatus() CatalogSourceStatus { if o == nil || IsNil(o.Status) { @@ -321,6 +358,9 @@ func (o CatalogSource) ToMap() (map[string]interface{}, error) { toSerialize["enabled"] = o.Enabled } toSerialize["labels"] = o.Labels + if !IsNil(o.AssetType) { + toSerialize["assetType"] = o.AssetType + } if !IsNil(o.Status) { toSerialize["status"] = o.Status } diff --git a/catalog/pkg/openapi/model_mcp_artifact.go b/catalog/pkg/openapi/model_mcp_artifact.go new file mode 100644 index 0000000000..1878c0052a --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_artifact.go @@ -0,0 +1,192 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpArtifact type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpArtifact{} + +// McpArtifact An artifact for an MCP server (e.g., OCI image for local deployment). +type McpArtifact struct { + // URI where the artifact can be retrieved (e.g., OCI image URI). + Uri string `json:"uri"` + // Timestamp when the artifact was created (milliseconds since epoch). + CreateTimeSinceEpoch *string `json:"createTimeSinceEpoch,omitempty"` + // Timestamp when the artifact was last updated (milliseconds since epoch). + LastUpdateTimeSinceEpoch *string `json:"lastUpdateTimeSinceEpoch,omitempty"` +} + +type _McpArtifact McpArtifact + +// NewMcpArtifact instantiates a new McpArtifact object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpArtifact(uri string) *McpArtifact { + this := McpArtifact{} + this.Uri = uri + return &this +} + +// NewMcpArtifactWithDefaults instantiates a new McpArtifact object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpArtifactWithDefaults() *McpArtifact { + this := McpArtifact{} + return &this +} + +// GetUri returns the Uri field value +func (o *McpArtifact) GetUri() string { + if o == nil { + var ret string + return ret + } + + return o.Uri +} + +// GetUriOk returns a tuple with the Uri field value +// and a boolean to check if the value has been set. +func (o *McpArtifact) GetUriOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Uri, true +} + +// SetUri sets field value +func (o *McpArtifact) SetUri(v string) { + o.Uri = v +} + +// GetCreateTimeSinceEpoch returns the CreateTimeSinceEpoch field value if set, zero value otherwise. +func (o *McpArtifact) GetCreateTimeSinceEpoch() string { + if o == nil || IsNil(o.CreateTimeSinceEpoch) { + var ret string + return ret + } + return *o.CreateTimeSinceEpoch +} + +// GetCreateTimeSinceEpochOk returns a tuple with the CreateTimeSinceEpoch field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpArtifact) GetCreateTimeSinceEpochOk() (*string, bool) { + if o == nil || IsNil(o.CreateTimeSinceEpoch) { + return nil, false + } + return o.CreateTimeSinceEpoch, true +} + +// HasCreateTimeSinceEpoch returns a boolean if a field has been set. +func (o *McpArtifact) HasCreateTimeSinceEpoch() bool { + if o != nil && !IsNil(o.CreateTimeSinceEpoch) { + return true + } + + return false +} + +// SetCreateTimeSinceEpoch gets a reference to the given string and assigns it to the CreateTimeSinceEpoch field. +func (o *McpArtifact) SetCreateTimeSinceEpoch(v string) { + o.CreateTimeSinceEpoch = &v +} + +// GetLastUpdateTimeSinceEpoch returns the LastUpdateTimeSinceEpoch field value if set, zero value otherwise. +func (o *McpArtifact) GetLastUpdateTimeSinceEpoch() string { + if o == nil || IsNil(o.LastUpdateTimeSinceEpoch) { + var ret string + return ret + } + return *o.LastUpdateTimeSinceEpoch +} + +// GetLastUpdateTimeSinceEpochOk returns a tuple with the LastUpdateTimeSinceEpoch field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpArtifact) GetLastUpdateTimeSinceEpochOk() (*string, bool) { + if o == nil || IsNil(o.LastUpdateTimeSinceEpoch) { + return nil, false + } + return o.LastUpdateTimeSinceEpoch, true +} + +// HasLastUpdateTimeSinceEpoch returns a boolean if a field has been set. +func (o *McpArtifact) HasLastUpdateTimeSinceEpoch() bool { + if o != nil && !IsNil(o.LastUpdateTimeSinceEpoch) { + return true + } + + return false +} + +// SetLastUpdateTimeSinceEpoch gets a reference to the given string and assigns it to the LastUpdateTimeSinceEpoch field. +func (o *McpArtifact) SetLastUpdateTimeSinceEpoch(v string) { + o.LastUpdateTimeSinceEpoch = &v +} + +func (o McpArtifact) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpArtifact) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["uri"] = o.Uri + if !IsNil(o.CreateTimeSinceEpoch) { + toSerialize["createTimeSinceEpoch"] = o.CreateTimeSinceEpoch + } + if !IsNil(o.LastUpdateTimeSinceEpoch) { + toSerialize["lastUpdateTimeSinceEpoch"] = o.LastUpdateTimeSinceEpoch + } + return toSerialize, nil +} + +type NullableMcpArtifact struct { + value *McpArtifact + isSet bool +} + +func (v NullableMcpArtifact) Get() *McpArtifact { + return v.value +} + +func (v *NullableMcpArtifact) Set(val *McpArtifact) { + v.value = val + v.isSet = true +} + +func (v NullableMcpArtifact) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpArtifact) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpArtifact(val *McpArtifact) *NullableMcpArtifact { + return &NullableMcpArtifact{value: val, isSet: true} +} + +func (v NullableMcpArtifact) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpArtifact) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_deployment_mode.go b/catalog/pkg/openapi/model_mcp_deployment_mode.go new file mode 100644 index 0000000000..bb390af5b4 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_deployment_mode.go @@ -0,0 +1,110 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "fmt" +) + +// McpDeploymentMode Deployment mode for the MCP server. - `local`: Server deployed from OCI artifact in Kubernetes cluster - `remote`: Server hosted externally, accessed via network endpoints +type McpDeploymentMode string + +// List of McpDeploymentMode +const ( + MCPDEPLOYMENTMODE_LOCAL McpDeploymentMode = "local" + MCPDEPLOYMENTMODE_REMOTE McpDeploymentMode = "remote" +) + +// All allowed values of McpDeploymentMode enum +var AllowedMcpDeploymentModeEnumValues = []McpDeploymentMode{ + "local", + "remote", +} + +func (v *McpDeploymentMode) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := McpDeploymentMode(value) + for _, existing := range AllowedMcpDeploymentModeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid McpDeploymentMode", value) +} + +// NewMcpDeploymentModeFromValue returns a pointer to a valid McpDeploymentMode +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewMcpDeploymentModeFromValue(v string) (*McpDeploymentMode, error) { + ev := McpDeploymentMode(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for McpDeploymentMode: valid values are %v", v, AllowedMcpDeploymentModeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v McpDeploymentMode) IsValid() bool { + for _, existing := range AllowedMcpDeploymentModeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to McpDeploymentMode value +func (v McpDeploymentMode) Ptr() *McpDeploymentMode { + return &v +} + +type NullableMcpDeploymentMode struct { + value *McpDeploymentMode + isSet bool +} + +func (v NullableMcpDeploymentMode) Get() *McpDeploymentMode { + return v.value +} + +func (v *NullableMcpDeploymentMode) Set(val *McpDeploymentMode) { + v.value = val + v.isSet = true +} + +func (v NullableMcpDeploymentMode) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpDeploymentMode) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpDeploymentMode(val *McpDeploymentMode) *NullableMcpDeploymentMode { + return &NullableMcpDeploymentMode{value: val, isSet: true} +} + +func (v NullableMcpDeploymentMode) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpDeploymentMode) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_endpoints.go b/catalog/pkg/openapi/model_mcp_endpoints.go new file mode 100644 index 0000000000..b4982b728d --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_endpoints.go @@ -0,0 +1,162 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpEndpoints type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpEndpoints{} + +// McpEndpoints Network endpoints for remote MCP servers. Contains URLs for different transport protocols. At least one endpoint must be provided for remote servers. +type McpEndpoints struct { + // HTTP REST endpoint URL. + Http *string `json:"http,omitempty"` + // Server-Sent Events endpoint URL. + Sse *string `json:"sse,omitempty"` +} + +// NewMcpEndpoints instantiates a new McpEndpoints object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpEndpoints() *McpEndpoints { + this := McpEndpoints{} + return &this +} + +// NewMcpEndpointsWithDefaults instantiates a new McpEndpoints object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpEndpointsWithDefaults() *McpEndpoints { + this := McpEndpoints{} + return &this +} + +// GetHttp returns the Http field value if set, zero value otherwise. +func (o *McpEndpoints) GetHttp() string { + if o == nil || IsNil(o.Http) { + var ret string + return ret + } + return *o.Http +} + +// GetHttpOk returns a tuple with the Http field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpEndpoints) GetHttpOk() (*string, bool) { + if o == nil || IsNil(o.Http) { + return nil, false + } + return o.Http, true +} + +// HasHttp returns a boolean if a field has been set. +func (o *McpEndpoints) HasHttp() bool { + if o != nil && !IsNil(o.Http) { + return true + } + + return false +} + +// SetHttp gets a reference to the given string and assigns it to the Http field. +func (o *McpEndpoints) SetHttp(v string) { + o.Http = &v +} + +// GetSse returns the Sse field value if set, zero value otherwise. +func (o *McpEndpoints) GetSse() string { + if o == nil || IsNil(o.Sse) { + var ret string + return ret + } + return *o.Sse +} + +// GetSseOk returns a tuple with the Sse field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpEndpoints) GetSseOk() (*string, bool) { + if o == nil || IsNil(o.Sse) { + return nil, false + } + return o.Sse, true +} + +// HasSse returns a boolean if a field has been set. +func (o *McpEndpoints) HasSse() bool { + if o != nil && !IsNil(o.Sse) { + return true + } + + return false +} + +// SetSse gets a reference to the given string and assigns it to the Sse field. +func (o *McpEndpoints) SetSse(v string) { + o.Sse = &v +} + +func (o McpEndpoints) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpEndpoints) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Http) { + toSerialize["http"] = o.Http + } + if !IsNil(o.Sse) { + toSerialize["sse"] = o.Sse + } + return toSerialize, nil +} + +type NullableMcpEndpoints struct { + value *McpEndpoints + isSet bool +} + +func (v NullableMcpEndpoints) Get() *McpEndpoints { + return v.value +} + +func (v *NullableMcpEndpoints) Set(val *McpEndpoints) { + v.value = val + v.isSet = true +} + +func (v NullableMcpEndpoints) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpEndpoints) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpEndpoints(val *McpEndpoints) *NullableMcpEndpoints { + return &NullableMcpEndpoints{value: val, isSet: true} +} + +func (v NullableMcpEndpoints) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpEndpoints) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_security_indicator.go b/catalog/pkg/openapi/model_mcp_security_indicator.go new file mode 100644 index 0000000000..6ba6b909d0 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_security_indicator.go @@ -0,0 +1,236 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpSecurityIndicator type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpSecurityIndicator{} + +// McpSecurityIndicator Security indicators for an MCP server. +type McpSecurityIndicator struct { + // Whether the source has been verified. + VerifiedSource *bool `json:"verifiedSource,omitempty"` + // Whether the endpoint uses secure communication. + SecureEndpoint *bool `json:"secureEndpoint,omitempty"` + // Whether static application security testing has been performed. + Sast *bool `json:"sast,omitempty"` + // Whether all tools are read-only. + ReadOnlyTools *bool `json:"readOnlyTools,omitempty"` +} + +// NewMcpSecurityIndicator instantiates a new McpSecurityIndicator object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpSecurityIndicator() *McpSecurityIndicator { + this := McpSecurityIndicator{} + return &this +} + +// NewMcpSecurityIndicatorWithDefaults instantiates a new McpSecurityIndicator object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpSecurityIndicatorWithDefaults() *McpSecurityIndicator { + this := McpSecurityIndicator{} + return &this +} + +// GetVerifiedSource returns the VerifiedSource field value if set, zero value otherwise. +func (o *McpSecurityIndicator) GetVerifiedSource() bool { + if o == nil || IsNil(o.VerifiedSource) { + var ret bool + return ret + } + return *o.VerifiedSource +} + +// GetVerifiedSourceOk returns a tuple with the VerifiedSource field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpSecurityIndicator) GetVerifiedSourceOk() (*bool, bool) { + if o == nil || IsNil(o.VerifiedSource) { + return nil, false + } + return o.VerifiedSource, true +} + +// HasVerifiedSource returns a boolean if a field has been set. +func (o *McpSecurityIndicator) HasVerifiedSource() bool { + if o != nil && !IsNil(o.VerifiedSource) { + return true + } + + return false +} + +// SetVerifiedSource gets a reference to the given bool and assigns it to the VerifiedSource field. +func (o *McpSecurityIndicator) SetVerifiedSource(v bool) { + o.VerifiedSource = &v +} + +// GetSecureEndpoint returns the SecureEndpoint field value if set, zero value otherwise. +func (o *McpSecurityIndicator) GetSecureEndpoint() bool { + if o == nil || IsNil(o.SecureEndpoint) { + var ret bool + return ret + } + return *o.SecureEndpoint +} + +// GetSecureEndpointOk returns a tuple with the SecureEndpoint field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpSecurityIndicator) GetSecureEndpointOk() (*bool, bool) { + if o == nil || IsNil(o.SecureEndpoint) { + return nil, false + } + return o.SecureEndpoint, true +} + +// HasSecureEndpoint returns a boolean if a field has been set. +func (o *McpSecurityIndicator) HasSecureEndpoint() bool { + if o != nil && !IsNil(o.SecureEndpoint) { + return true + } + + return false +} + +// SetSecureEndpoint gets a reference to the given bool and assigns it to the SecureEndpoint field. +func (o *McpSecurityIndicator) SetSecureEndpoint(v bool) { + o.SecureEndpoint = &v +} + +// GetSast returns the Sast field value if set, zero value otherwise. +func (o *McpSecurityIndicator) GetSast() bool { + if o == nil || IsNil(o.Sast) { + var ret bool + return ret + } + return *o.Sast +} + +// GetSastOk returns a tuple with the Sast field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpSecurityIndicator) GetSastOk() (*bool, bool) { + if o == nil || IsNil(o.Sast) { + return nil, false + } + return o.Sast, true +} + +// HasSast returns a boolean if a field has been set. +func (o *McpSecurityIndicator) HasSast() bool { + if o != nil && !IsNil(o.Sast) { + return true + } + + return false +} + +// SetSast gets a reference to the given bool and assigns it to the Sast field. +func (o *McpSecurityIndicator) SetSast(v bool) { + o.Sast = &v +} + +// GetReadOnlyTools returns the ReadOnlyTools field value if set, zero value otherwise. +func (o *McpSecurityIndicator) GetReadOnlyTools() bool { + if o == nil || IsNil(o.ReadOnlyTools) { + var ret bool + return ret + } + return *o.ReadOnlyTools +} + +// GetReadOnlyToolsOk returns a tuple with the ReadOnlyTools field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpSecurityIndicator) GetReadOnlyToolsOk() (*bool, bool) { + if o == nil || IsNil(o.ReadOnlyTools) { + return nil, false + } + return o.ReadOnlyTools, true +} + +// HasReadOnlyTools returns a boolean if a field has been set. +func (o *McpSecurityIndicator) HasReadOnlyTools() bool { + if o != nil && !IsNil(o.ReadOnlyTools) { + return true + } + + return false +} + +// SetReadOnlyTools gets a reference to the given bool and assigns it to the ReadOnlyTools field. +func (o *McpSecurityIndicator) SetReadOnlyTools(v bool) { + o.ReadOnlyTools = &v +} + +func (o McpSecurityIndicator) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpSecurityIndicator) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.VerifiedSource) { + toSerialize["verifiedSource"] = o.VerifiedSource + } + if !IsNil(o.SecureEndpoint) { + toSerialize["secureEndpoint"] = o.SecureEndpoint + } + if !IsNil(o.Sast) { + toSerialize["sast"] = o.Sast + } + if !IsNil(o.ReadOnlyTools) { + toSerialize["readOnlyTools"] = o.ReadOnlyTools + } + return toSerialize, nil +} + +type NullableMcpSecurityIndicator struct { + value *McpSecurityIndicator + isSet bool +} + +func (v NullableMcpSecurityIndicator) Get() *McpSecurityIndicator { + return v.value +} + +func (v *NullableMcpSecurityIndicator) Set(val *McpSecurityIndicator) { + v.value = val + v.isSet = true +} + +func (v NullableMcpSecurityIndicator) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpSecurityIndicator) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpSecurityIndicator(val *McpSecurityIndicator) *NullableMcpSecurityIndicator { + return &NullableMcpSecurityIndicator{value: val, isSet: true} +} + +func (v NullableMcpSecurityIndicator) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpSecurityIndicator) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_server.go b/catalog/pkg/openapi/model_mcp_server.go new file mode 100644 index 0000000000..10974054fb --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_server.go @@ -0,0 +1,925 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "time" +) + +// checks if the McpServer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpServer{} + +// McpServer An MCP (Model Context Protocol) server that provides tools for AI agents. +type McpServer struct { + // Unique identifier for the MCP server. + Id string `json:"id"` + // Human-readable name of the MCP server. + Name string `json:"name"` + // ID of the catalog source this server belongs to. + SourceId *string `json:"source_id,omitempty"` + // Description of the MCP server and its capabilities. + Description *string `json:"description,omitempty"` + // URL to the server's logo image. + Logo *string `json:"logo,omitempty"` + // License identifier (SPDX format preferred) under which the MCP server is distributed. + License *string `json:"license,omitempty"` + // URL to the full license text or license file. + LicenseLink *string `json:"license_link,omitempty"` + // Organization or entity that provides the MCP server. + Provider *string `json:"provider,omitempty"` + // Version of the MCP server. + Version *string `json:"version,omitempty"` + // Tags for categorizing and filtering MCP servers. + Tags []string `json:"tags,omitempty"` + // List of tools exposed by this MCP server. + Tools []McpTool `json:"tools,omitempty"` + SecurityIndicators *McpSecurityIndicator `json:"securityIndicators,omitempty"` + // URL to the server's documentation. + DocumentationUrl *string `json:"documentationUrl,omitempty"` + // URL to the server's source repository. + RepositoryUrl *string `json:"repositoryUrl,omitempty"` + // Source code repository identifier (e.g., GitHub org/repo). + SourceCode *string `json:"sourceCode,omitempty"` + // When the server was last updated. + LastUpdated *time.Time `json:"lastUpdated,omitempty"` + // When the server was first published. + PublishedDate *string `json:"publishedDate,omitempty"` + // Artifacts for this MCP server (e.g., OCI images for local deployments). + Artifacts []McpArtifact `json:"artifacts,omitempty"` + // Supported transport types. For remote servers, this is derived from available endpoints. + Transports []McpTransportType `json:"transports,omitempty"` + // Full README content in Markdown format. + Readme *string `json:"readme,omitempty"` + DeploymentMode *McpDeploymentMode `json:"deploymentMode,omitempty"` + Endpoints *McpEndpoints `json:"endpoints,omitempty"` + // User provided custom properties which are not defined by its type. Following the Model Registry pattern, tags are stored as MetadataStringValue entries with empty string_value, and security indicators are stored as MetadataBoolValue entries. + CustomProperties map[string]MetadataValue `json:"customProperties,omitempty"` +} + +type _McpServer McpServer + +// NewMcpServer instantiates a new McpServer object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpServer(id string, name string) *McpServer { + this := McpServer{} + this.Id = id + this.Name = name + var deploymentMode McpDeploymentMode = MCPDEPLOYMENTMODE_LOCAL + this.DeploymentMode = &deploymentMode + return &this +} + +// NewMcpServerWithDefaults instantiates a new McpServer object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpServerWithDefaults() *McpServer { + this := McpServer{} + var deploymentMode McpDeploymentMode = MCPDEPLOYMENTMODE_LOCAL + this.DeploymentMode = &deploymentMode + return &this +} + +// GetId returns the Id field value +func (o *McpServer) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *McpServer) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *McpServer) SetId(v string) { + o.Id = v +} + +// GetName returns the Name field value +func (o *McpServer) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *McpServer) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *McpServer) SetName(v string) { + o.Name = v +} + +// GetSourceId returns the SourceId field value if set, zero value otherwise. +func (o *McpServer) GetSourceId() string { + if o == nil || IsNil(o.SourceId) { + var ret string + return ret + } + return *o.SourceId +} + +// GetSourceIdOk returns a tuple with the SourceId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetSourceIdOk() (*string, bool) { + if o == nil || IsNil(o.SourceId) { + return nil, false + } + return o.SourceId, true +} + +// HasSourceId returns a boolean if a field has been set. +func (o *McpServer) HasSourceId() bool { + if o != nil && !IsNil(o.SourceId) { + return true + } + + return false +} + +// SetSourceId gets a reference to the given string and assigns it to the SourceId field. +func (o *McpServer) SetSourceId(v string) { + o.SourceId = &v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *McpServer) GetDescription() string { + if o == nil || IsNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetDescriptionOk() (*string, bool) { + if o == nil || IsNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *McpServer) HasDescription() bool { + if o != nil && !IsNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *McpServer) SetDescription(v string) { + o.Description = &v +} + +// GetLogo returns the Logo field value if set, zero value otherwise. +func (o *McpServer) GetLogo() string { + if o == nil || IsNil(o.Logo) { + var ret string + return ret + } + return *o.Logo +} + +// GetLogoOk returns a tuple with the Logo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetLogoOk() (*string, bool) { + if o == nil || IsNil(o.Logo) { + return nil, false + } + return o.Logo, true +} + +// HasLogo returns a boolean if a field has been set. +func (o *McpServer) HasLogo() bool { + if o != nil && !IsNil(o.Logo) { + return true + } + + return false +} + +// SetLogo gets a reference to the given string and assigns it to the Logo field. +func (o *McpServer) SetLogo(v string) { + o.Logo = &v +} + +// GetLicense returns the License field value if set, zero value otherwise. +func (o *McpServer) GetLicense() string { + if o == nil || IsNil(o.License) { + var ret string + return ret + } + return *o.License +} + +// GetLicenseOk returns a tuple with the License field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetLicenseOk() (*string, bool) { + if o == nil || IsNil(o.License) { + return nil, false + } + return o.License, true +} + +// HasLicense returns a boolean if a field has been set. +func (o *McpServer) HasLicense() bool { + if o != nil && !IsNil(o.License) { + return true + } + + return false +} + +// SetLicense gets a reference to the given string and assigns it to the License field. +func (o *McpServer) SetLicense(v string) { + o.License = &v +} + +// GetLicenseLink returns the LicenseLink field value if set, zero value otherwise. +func (o *McpServer) GetLicenseLink() string { + if o == nil || IsNil(o.LicenseLink) { + var ret string + return ret + } + return *o.LicenseLink +} + +// GetLicenseLinkOk returns a tuple with the LicenseLink field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetLicenseLinkOk() (*string, bool) { + if o == nil || IsNil(o.LicenseLink) { + return nil, false + } + return o.LicenseLink, true +} + +// HasLicenseLink returns a boolean if a field has been set. +func (o *McpServer) HasLicenseLink() bool { + if o != nil && !IsNil(o.LicenseLink) { + return true + } + + return false +} + +// SetLicenseLink gets a reference to the given string and assigns it to the LicenseLink field. +func (o *McpServer) SetLicenseLink(v string) { + o.LicenseLink = &v +} + +// GetProvider returns the Provider field value if set, zero value otherwise. +func (o *McpServer) GetProvider() string { + if o == nil || IsNil(o.Provider) { + var ret string + return ret + } + return *o.Provider +} + +// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetProviderOk() (*string, bool) { + if o == nil || IsNil(o.Provider) { + return nil, false + } + return o.Provider, true +} + +// HasProvider returns a boolean if a field has been set. +func (o *McpServer) HasProvider() bool { + if o != nil && !IsNil(o.Provider) { + return true + } + + return false +} + +// SetProvider gets a reference to the given string and assigns it to the Provider field. +func (o *McpServer) SetProvider(v string) { + o.Provider = &v +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *McpServer) GetVersion() string { + if o == nil || IsNil(o.Version) { + var ret string + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetVersionOk() (*string, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *McpServer) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given string and assigns it to the Version field. +func (o *McpServer) SetVersion(v string) { + o.Version = &v +} + +// GetTags returns the Tags field value if set, zero value otherwise. +func (o *McpServer) GetTags() []string { + if o == nil || IsNil(o.Tags) { + var ret []string + return ret + } + return o.Tags +} + +// GetTagsOk returns a tuple with the Tags field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetTagsOk() ([]string, bool) { + if o == nil || IsNil(o.Tags) { + return nil, false + } + return o.Tags, true +} + +// HasTags returns a boolean if a field has been set. +func (o *McpServer) HasTags() bool { + if o != nil && !IsNil(o.Tags) { + return true + } + + return false +} + +// SetTags gets a reference to the given []string and assigns it to the Tags field. +func (o *McpServer) SetTags(v []string) { + o.Tags = v +} + +// GetTools returns the Tools field value if set, zero value otherwise. +func (o *McpServer) GetTools() []McpTool { + if o == nil || IsNil(o.Tools) { + var ret []McpTool + return ret + } + return o.Tools +} + +// GetToolsOk returns a tuple with the Tools field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetToolsOk() ([]McpTool, bool) { + if o == nil || IsNil(o.Tools) { + return nil, false + } + return o.Tools, true +} + +// HasTools returns a boolean if a field has been set. +func (o *McpServer) HasTools() bool { + if o != nil && !IsNil(o.Tools) { + return true + } + + return false +} + +// SetTools gets a reference to the given []McpTool and assigns it to the Tools field. +func (o *McpServer) SetTools(v []McpTool) { + o.Tools = v +} + +// GetSecurityIndicators returns the SecurityIndicators field value if set, zero value otherwise. +func (o *McpServer) GetSecurityIndicators() McpSecurityIndicator { + if o == nil || IsNil(o.SecurityIndicators) { + var ret McpSecurityIndicator + return ret + } + return *o.SecurityIndicators +} + +// GetSecurityIndicatorsOk returns a tuple with the SecurityIndicators field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetSecurityIndicatorsOk() (*McpSecurityIndicator, bool) { + if o == nil || IsNil(o.SecurityIndicators) { + return nil, false + } + return o.SecurityIndicators, true +} + +// HasSecurityIndicators returns a boolean if a field has been set. +func (o *McpServer) HasSecurityIndicators() bool { + if o != nil && !IsNil(o.SecurityIndicators) { + return true + } + + return false +} + +// SetSecurityIndicators gets a reference to the given McpSecurityIndicator and assigns it to the SecurityIndicators field. +func (o *McpServer) SetSecurityIndicators(v McpSecurityIndicator) { + o.SecurityIndicators = &v +} + +// GetDocumentationUrl returns the DocumentationUrl field value if set, zero value otherwise. +func (o *McpServer) GetDocumentationUrl() string { + if o == nil || IsNil(o.DocumentationUrl) { + var ret string + return ret + } + return *o.DocumentationUrl +} + +// GetDocumentationUrlOk returns a tuple with the DocumentationUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetDocumentationUrlOk() (*string, bool) { + if o == nil || IsNil(o.DocumentationUrl) { + return nil, false + } + return o.DocumentationUrl, true +} + +// HasDocumentationUrl returns a boolean if a field has been set. +func (o *McpServer) HasDocumentationUrl() bool { + if o != nil && !IsNil(o.DocumentationUrl) { + return true + } + + return false +} + +// SetDocumentationUrl gets a reference to the given string and assigns it to the DocumentationUrl field. +func (o *McpServer) SetDocumentationUrl(v string) { + o.DocumentationUrl = &v +} + +// GetRepositoryUrl returns the RepositoryUrl field value if set, zero value otherwise. +func (o *McpServer) GetRepositoryUrl() string { + if o == nil || IsNil(o.RepositoryUrl) { + var ret string + return ret + } + return *o.RepositoryUrl +} + +// GetRepositoryUrlOk returns a tuple with the RepositoryUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetRepositoryUrlOk() (*string, bool) { + if o == nil || IsNil(o.RepositoryUrl) { + return nil, false + } + return o.RepositoryUrl, true +} + +// HasRepositoryUrl returns a boolean if a field has been set. +func (o *McpServer) HasRepositoryUrl() bool { + if o != nil && !IsNil(o.RepositoryUrl) { + return true + } + + return false +} + +// SetRepositoryUrl gets a reference to the given string and assigns it to the RepositoryUrl field. +func (o *McpServer) SetRepositoryUrl(v string) { + o.RepositoryUrl = &v +} + +// GetSourceCode returns the SourceCode field value if set, zero value otherwise. +func (o *McpServer) GetSourceCode() string { + if o == nil || IsNil(o.SourceCode) { + var ret string + return ret + } + return *o.SourceCode +} + +// GetSourceCodeOk returns a tuple with the SourceCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetSourceCodeOk() (*string, bool) { + if o == nil || IsNil(o.SourceCode) { + return nil, false + } + return o.SourceCode, true +} + +// HasSourceCode returns a boolean if a field has been set. +func (o *McpServer) HasSourceCode() bool { + if o != nil && !IsNil(o.SourceCode) { + return true + } + + return false +} + +// SetSourceCode gets a reference to the given string and assigns it to the SourceCode field. +func (o *McpServer) SetSourceCode(v string) { + o.SourceCode = &v +} + +// GetLastUpdated returns the LastUpdated field value if set, zero value otherwise. +func (o *McpServer) GetLastUpdated() time.Time { + if o == nil || IsNil(o.LastUpdated) { + var ret time.Time + return ret + } + return *o.LastUpdated +} + +// GetLastUpdatedOk returns a tuple with the LastUpdated field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetLastUpdatedOk() (*time.Time, bool) { + if o == nil || IsNil(o.LastUpdated) { + return nil, false + } + return o.LastUpdated, true +} + +// HasLastUpdated returns a boolean if a field has been set. +func (o *McpServer) HasLastUpdated() bool { + if o != nil && !IsNil(o.LastUpdated) { + return true + } + + return false +} + +// SetLastUpdated gets a reference to the given time.Time and assigns it to the LastUpdated field. +func (o *McpServer) SetLastUpdated(v time.Time) { + o.LastUpdated = &v +} + +// GetPublishedDate returns the PublishedDate field value if set, zero value otherwise. +func (o *McpServer) GetPublishedDate() string { + if o == nil || IsNil(o.PublishedDate) { + var ret string + return ret + } + return *o.PublishedDate +} + +// GetPublishedDateOk returns a tuple with the PublishedDate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetPublishedDateOk() (*string, bool) { + if o == nil || IsNil(o.PublishedDate) { + return nil, false + } + return o.PublishedDate, true +} + +// HasPublishedDate returns a boolean if a field has been set. +func (o *McpServer) HasPublishedDate() bool { + if o != nil && !IsNil(o.PublishedDate) { + return true + } + + return false +} + +// SetPublishedDate gets a reference to the given string and assigns it to the PublishedDate field. +func (o *McpServer) SetPublishedDate(v string) { + o.PublishedDate = &v +} + +// GetArtifacts returns the Artifacts field value if set, zero value otherwise. +func (o *McpServer) GetArtifacts() []McpArtifact { + if o == nil || IsNil(o.Artifacts) { + var ret []McpArtifact + return ret + } + return o.Artifacts +} + +// GetArtifactsOk returns a tuple with the Artifacts field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetArtifactsOk() ([]McpArtifact, bool) { + if o == nil || IsNil(o.Artifacts) { + return nil, false + } + return o.Artifacts, true +} + +// HasArtifacts returns a boolean if a field has been set. +func (o *McpServer) HasArtifacts() bool { + if o != nil && !IsNil(o.Artifacts) { + return true + } + + return false +} + +// SetArtifacts gets a reference to the given []McpArtifact and assigns it to the Artifacts field. +func (o *McpServer) SetArtifacts(v []McpArtifact) { + o.Artifacts = v +} + +// GetTransports returns the Transports field value if set, zero value otherwise. +func (o *McpServer) GetTransports() []McpTransportType { + if o == nil || IsNil(o.Transports) { + var ret []McpTransportType + return ret + } + return o.Transports +} + +// GetTransportsOk returns a tuple with the Transports field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetTransportsOk() ([]McpTransportType, bool) { + if o == nil || IsNil(o.Transports) { + return nil, false + } + return o.Transports, true +} + +// HasTransports returns a boolean if a field has been set. +func (o *McpServer) HasTransports() bool { + if o != nil && !IsNil(o.Transports) { + return true + } + + return false +} + +// SetTransports gets a reference to the given []McpTransportType and assigns it to the Transports field. +func (o *McpServer) SetTransports(v []McpTransportType) { + o.Transports = v +} + +// GetReadme returns the Readme field value if set, zero value otherwise. +func (o *McpServer) GetReadme() string { + if o == nil || IsNil(o.Readme) { + var ret string + return ret + } + return *o.Readme +} + +// GetReadmeOk returns a tuple with the Readme field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetReadmeOk() (*string, bool) { + if o == nil || IsNil(o.Readme) { + return nil, false + } + return o.Readme, true +} + +// HasReadme returns a boolean if a field has been set. +func (o *McpServer) HasReadme() bool { + if o != nil && !IsNil(o.Readme) { + return true + } + + return false +} + +// SetReadme gets a reference to the given string and assigns it to the Readme field. +func (o *McpServer) SetReadme(v string) { + o.Readme = &v +} + +// GetDeploymentMode returns the DeploymentMode field value if set, zero value otherwise. +func (o *McpServer) GetDeploymentMode() McpDeploymentMode { + if o == nil || IsNil(o.DeploymentMode) { + var ret McpDeploymentMode + return ret + } + return *o.DeploymentMode +} + +// GetDeploymentModeOk returns a tuple with the DeploymentMode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetDeploymentModeOk() (*McpDeploymentMode, bool) { + if o == nil || IsNil(o.DeploymentMode) { + return nil, false + } + return o.DeploymentMode, true +} + +// HasDeploymentMode returns a boolean if a field has been set. +func (o *McpServer) HasDeploymentMode() bool { + if o != nil && !IsNil(o.DeploymentMode) { + return true + } + + return false +} + +// SetDeploymentMode gets a reference to the given McpDeploymentMode and assigns it to the DeploymentMode field. +func (o *McpServer) SetDeploymentMode(v McpDeploymentMode) { + o.DeploymentMode = &v +} + +// GetEndpoints returns the Endpoints field value if set, zero value otherwise. +func (o *McpServer) GetEndpoints() McpEndpoints { + if o == nil || IsNil(o.Endpoints) { + var ret McpEndpoints + return ret + } + return *o.Endpoints +} + +// GetEndpointsOk returns a tuple with the Endpoints field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetEndpointsOk() (*McpEndpoints, bool) { + if o == nil || IsNil(o.Endpoints) { + return nil, false + } + return o.Endpoints, true +} + +// HasEndpoints returns a boolean if a field has been set. +func (o *McpServer) HasEndpoints() bool { + if o != nil && !IsNil(o.Endpoints) { + return true + } + + return false +} + +// SetEndpoints gets a reference to the given McpEndpoints and assigns it to the Endpoints field. +func (o *McpServer) SetEndpoints(v McpEndpoints) { + o.Endpoints = &v +} + +// GetCustomProperties returns the CustomProperties field value if set, zero value otherwise. +func (o *McpServer) GetCustomProperties() map[string]MetadataValue { + if o == nil || IsNil(o.CustomProperties) { + var ret map[string]MetadataValue + return ret + } + return o.CustomProperties +} + +// GetCustomPropertiesOk returns a tuple with the CustomProperties field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpServer) GetCustomPropertiesOk() (map[string]MetadataValue, bool) { + if o == nil || IsNil(o.CustomProperties) { + return map[string]MetadataValue{}, false + } + return o.CustomProperties, true +} + +// HasCustomProperties returns a boolean if a field has been set. +func (o *McpServer) HasCustomProperties() bool { + if o != nil && !IsNil(o.CustomProperties) { + return true + } + + return false +} + +// SetCustomProperties gets a reference to the given map[string]MetadataValue and assigns it to the CustomProperties field. +func (o *McpServer) SetCustomProperties(v map[string]MetadataValue) { + o.CustomProperties = v +} + +func (o McpServer) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpServer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["name"] = o.Name + if !IsNil(o.SourceId) { + toSerialize["source_id"] = o.SourceId + } + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + if !IsNil(o.Logo) { + toSerialize["logo"] = o.Logo + } + if !IsNil(o.License) { + toSerialize["license"] = o.License + } + if !IsNil(o.LicenseLink) { + toSerialize["license_link"] = o.LicenseLink + } + if !IsNil(o.Provider) { + toSerialize["provider"] = o.Provider + } + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + if !IsNil(o.Tags) { + toSerialize["tags"] = o.Tags + } + if !IsNil(o.Tools) { + toSerialize["tools"] = o.Tools + } + if !IsNil(o.SecurityIndicators) { + toSerialize["securityIndicators"] = o.SecurityIndicators + } + if !IsNil(o.DocumentationUrl) { + toSerialize["documentationUrl"] = o.DocumentationUrl + } + if !IsNil(o.RepositoryUrl) { + toSerialize["repositoryUrl"] = o.RepositoryUrl + } + if !IsNil(o.SourceCode) { + toSerialize["sourceCode"] = o.SourceCode + } + if !IsNil(o.LastUpdated) { + toSerialize["lastUpdated"] = o.LastUpdated + } + if !IsNil(o.PublishedDate) { + toSerialize["publishedDate"] = o.PublishedDate + } + if !IsNil(o.Artifacts) { + toSerialize["artifacts"] = o.Artifacts + } + if !IsNil(o.Transports) { + toSerialize["transports"] = o.Transports + } + if !IsNil(o.Readme) { + toSerialize["readme"] = o.Readme + } + if !IsNil(o.DeploymentMode) { + toSerialize["deploymentMode"] = o.DeploymentMode + } + if !IsNil(o.Endpoints) { + toSerialize["endpoints"] = o.Endpoints + } + if !IsNil(o.CustomProperties) { + toSerialize["customProperties"] = o.CustomProperties + } + return toSerialize, nil +} + +type NullableMcpServer struct { + value *McpServer + isSet bool +} + +func (v NullableMcpServer) Get() *McpServer { + return v.value +} + +func (v *NullableMcpServer) Set(val *McpServer) { + v.value = val + v.isSet = true +} + +func (v NullableMcpServer) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpServer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpServer(val *McpServer) *NullableMcpServer { + return &NullableMcpServer{value: val, isSet: true} +} + +func (v NullableMcpServer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpServer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_server_list.go b/catalog/pkg/openapi/model_mcp_server_list.go new file mode 100644 index 0000000000..2b5eb0186e --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_server_list.go @@ -0,0 +1,202 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpServerList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpServerList{} + +// McpServerList List of McpServer entities. +type McpServerList struct { + // Token to use to retrieve next page of results. + NextPageToken string `json:"nextPageToken"` + // Maximum number of resources to return in the result. + PageSize int32 `json:"pageSize"` + // Number of items in result list. + Size int32 `json:"size"` + // Array of `McpServer` entities. + Items []McpServer `json:"items"` +} + +type _McpServerList McpServerList + +// NewMcpServerList instantiates a new McpServerList object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpServerList(nextPageToken string, pageSize int32, size int32, items []McpServer) *McpServerList { + this := McpServerList{} + this.NextPageToken = nextPageToken + this.PageSize = pageSize + this.Size = size + this.Items = items + return &this +} + +// NewMcpServerListWithDefaults instantiates a new McpServerList object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpServerListWithDefaults() *McpServerList { + this := McpServerList{} + return &this +} + +// GetNextPageToken returns the NextPageToken field value +func (o *McpServerList) GetNextPageToken() string { + if o == nil { + var ret string + return ret + } + + return o.NextPageToken +} + +// GetNextPageTokenOk returns a tuple with the NextPageToken field value +// and a boolean to check if the value has been set. +func (o *McpServerList) GetNextPageTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.NextPageToken, true +} + +// SetNextPageToken sets field value +func (o *McpServerList) SetNextPageToken(v string) { + o.NextPageToken = v +} + +// GetPageSize returns the PageSize field value +func (o *McpServerList) GetPageSize() int32 { + if o == nil { + var ret int32 + return ret + } + + return o.PageSize +} + +// GetPageSizeOk returns a tuple with the PageSize field value +// and a boolean to check if the value has been set. +func (o *McpServerList) GetPageSizeOk() (*int32, bool) { + if o == nil { + return nil, false + } + return &o.PageSize, true +} + +// SetPageSize sets field value +func (o *McpServerList) SetPageSize(v int32) { + o.PageSize = v +} + +// GetSize returns the Size field value +func (o *McpServerList) GetSize() int32 { + if o == nil { + var ret int32 + return ret + } + + return o.Size +} + +// GetSizeOk returns a tuple with the Size field value +// and a boolean to check if the value has been set. +func (o *McpServerList) GetSizeOk() (*int32, bool) { + if o == nil { + return nil, false + } + return &o.Size, true +} + +// SetSize sets field value +func (o *McpServerList) SetSize(v int32) { + o.Size = v +} + +// GetItems returns the Items field value +func (o *McpServerList) GetItems() []McpServer { + if o == nil { + var ret []McpServer + return ret + } + + return o.Items +} + +// GetItemsOk returns a tuple with the Items field value +// and a boolean to check if the value has been set. +func (o *McpServerList) GetItemsOk() ([]McpServer, bool) { + if o == nil { + return nil, false + } + return o.Items, true +} + +// SetItems sets field value +func (o *McpServerList) SetItems(v []McpServer) { + o.Items = v +} + +func (o McpServerList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpServerList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["nextPageToken"] = o.NextPageToken + toSerialize["pageSize"] = o.PageSize + toSerialize["size"] = o.Size + toSerialize["items"] = o.Items + return toSerialize, nil +} + +type NullableMcpServerList struct { + value *McpServerList + isSet bool +} + +func (v NullableMcpServerList) Get() *McpServerList { + return v.value +} + +func (v *NullableMcpServerList) Set(val *McpServerList) { + v.value = val + v.isSet = true +} + +func (v NullableMcpServerList) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpServerList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpServerList(val *McpServerList) *NullableMcpServerList { + return &NullableMcpServerList{value: val, isSet: true} +} + +func (v NullableMcpServerList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpServerList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_server_status.go b/catalog/pkg/openapi/model_mcp_server_status.go new file mode 100644 index 0000000000..18ac6fccac --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_server_status.go @@ -0,0 +1,112 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "fmt" +) + +// McpServerStatus Availability status of an MCP server. - `AVAILABLE`: The server is ready to use - `ACCESS_REQUIRED`: The server requires authentication or access approval - `UNAVAILABLE`: The server is not currently available +type McpServerStatus string + +// List of McpServerStatus +const ( + MCPSERVERSTATUS_AVAILABLE McpServerStatus = "AVAILABLE" + MCPSERVERSTATUS_ACCESS_REQUIRED McpServerStatus = "ACCESS_REQUIRED" + MCPSERVERSTATUS_UNAVAILABLE McpServerStatus = "UNAVAILABLE" +) + +// All allowed values of McpServerStatus enum +var AllowedMcpServerStatusEnumValues = []McpServerStatus{ + "AVAILABLE", + "ACCESS_REQUIRED", + "UNAVAILABLE", +} + +func (v *McpServerStatus) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := McpServerStatus(value) + for _, existing := range AllowedMcpServerStatusEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid McpServerStatus", value) +} + +// NewMcpServerStatusFromValue returns a pointer to a valid McpServerStatus +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewMcpServerStatusFromValue(v string) (*McpServerStatus, error) { + ev := McpServerStatus(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for McpServerStatus: valid values are %v", v, AllowedMcpServerStatusEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v McpServerStatus) IsValid() bool { + for _, existing := range AllowedMcpServerStatusEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to McpServerStatus value +func (v McpServerStatus) Ptr() *McpServerStatus { + return &v +} + +type NullableMcpServerStatus struct { + value *McpServerStatus + isSet bool +} + +func (v NullableMcpServerStatus) Get() *McpServerStatus { + return v.value +} + +func (v *NullableMcpServerStatus) Set(val *McpServerStatus) { + v.value = val + v.isSet = true +} + +func (v NullableMcpServerStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpServerStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpServerStatus(val *McpServerStatus) *NullableMcpServerStatus { + return &NullableMcpServerStatus{value: val, isSet: true} +} + +func (v NullableMcpServerStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpServerStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_tool.go b/catalog/pkg/openapi/model_mcp_tool.go new file mode 100644 index 0000000000..5bdc162754 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_tool.go @@ -0,0 +1,334 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpTool type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpTool{} + +// McpTool A tool exposed by an MCP server. +type McpTool struct { + // Unique name of the tool within the MCP server. + Name string `json:"name"` + // Description of what the tool does. + Description *string `json:"description,omitempty"` + AccessType McpToolAccessType `json:"accessType"` + // Parameters accepted by this tool. + Parameters []McpToolParameter `json:"parameters,omitempty"` + // Whether this tool has been revoked. Revoked tools should not be invoked by AI agents. This allows for immediate disabling of problematic tools without removing them from the registry. + Revoked *bool `json:"revoked,omitempty"` + // Human-readable reason why the tool was revoked. This helps users understand why the tool is unavailable and when it might be restored. + RevokedReason *string `json:"revokedReason,omitempty"` + // User provided custom properties for the tool. + CustomProperties map[string]MetadataValue `json:"customProperties,omitempty"` +} + +type _McpTool McpTool + +// NewMcpTool instantiates a new McpTool object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpTool(name string, accessType McpToolAccessType) *McpTool { + this := McpTool{} + this.Name = name + this.AccessType = accessType + var revoked bool = false + this.Revoked = &revoked + return &this +} + +// NewMcpToolWithDefaults instantiates a new McpTool object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpToolWithDefaults() *McpTool { + this := McpTool{} + var revoked bool = false + this.Revoked = &revoked + return &this +} + +// GetName returns the Name field value +func (o *McpTool) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *McpTool) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *McpTool) SetName(v string) { + o.Name = v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *McpTool) GetDescription() string { + if o == nil || IsNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpTool) GetDescriptionOk() (*string, bool) { + if o == nil || IsNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *McpTool) HasDescription() bool { + if o != nil && !IsNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *McpTool) SetDescription(v string) { + o.Description = &v +} + +// GetAccessType returns the AccessType field value +func (o *McpTool) GetAccessType() McpToolAccessType { + if o == nil { + var ret McpToolAccessType + return ret + } + + return o.AccessType +} + +// GetAccessTypeOk returns a tuple with the AccessType field value +// and a boolean to check if the value has been set. +func (o *McpTool) GetAccessTypeOk() (*McpToolAccessType, bool) { + if o == nil { + return nil, false + } + return &o.AccessType, true +} + +// SetAccessType sets field value +func (o *McpTool) SetAccessType(v McpToolAccessType) { + o.AccessType = v +} + +// GetParameters returns the Parameters field value if set, zero value otherwise. +func (o *McpTool) GetParameters() []McpToolParameter { + if o == nil || IsNil(o.Parameters) { + var ret []McpToolParameter + return ret + } + return o.Parameters +} + +// GetParametersOk returns a tuple with the Parameters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpTool) GetParametersOk() ([]McpToolParameter, bool) { + if o == nil || IsNil(o.Parameters) { + return nil, false + } + return o.Parameters, true +} + +// HasParameters returns a boolean if a field has been set. +func (o *McpTool) HasParameters() bool { + if o != nil && !IsNil(o.Parameters) { + return true + } + + return false +} + +// SetParameters gets a reference to the given []McpToolParameter and assigns it to the Parameters field. +func (o *McpTool) SetParameters(v []McpToolParameter) { + o.Parameters = v +} + +// GetRevoked returns the Revoked field value if set, zero value otherwise. +func (o *McpTool) GetRevoked() bool { + if o == nil || IsNil(o.Revoked) { + var ret bool + return ret + } + return *o.Revoked +} + +// GetRevokedOk returns a tuple with the Revoked field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpTool) GetRevokedOk() (*bool, bool) { + if o == nil || IsNil(o.Revoked) { + return nil, false + } + return o.Revoked, true +} + +// HasRevoked returns a boolean if a field has been set. +func (o *McpTool) HasRevoked() bool { + if o != nil && !IsNil(o.Revoked) { + return true + } + + return false +} + +// SetRevoked gets a reference to the given bool and assigns it to the Revoked field. +func (o *McpTool) SetRevoked(v bool) { + o.Revoked = &v +} + +// GetRevokedReason returns the RevokedReason field value if set, zero value otherwise. +func (o *McpTool) GetRevokedReason() string { + if o == nil || IsNil(o.RevokedReason) { + var ret string + return ret + } + return *o.RevokedReason +} + +// GetRevokedReasonOk returns a tuple with the RevokedReason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpTool) GetRevokedReasonOk() (*string, bool) { + if o == nil || IsNil(o.RevokedReason) { + return nil, false + } + return o.RevokedReason, true +} + +// HasRevokedReason returns a boolean if a field has been set. +func (o *McpTool) HasRevokedReason() bool { + if o != nil && !IsNil(o.RevokedReason) { + return true + } + + return false +} + +// SetRevokedReason gets a reference to the given string and assigns it to the RevokedReason field. +func (o *McpTool) SetRevokedReason(v string) { + o.RevokedReason = &v +} + +// GetCustomProperties returns the CustomProperties field value if set, zero value otherwise. +func (o *McpTool) GetCustomProperties() map[string]MetadataValue { + if o == nil || IsNil(o.CustomProperties) { + var ret map[string]MetadataValue + return ret + } + return o.CustomProperties +} + +// GetCustomPropertiesOk returns a tuple with the CustomProperties field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpTool) GetCustomPropertiesOk() (map[string]MetadataValue, bool) { + if o == nil || IsNil(o.CustomProperties) { + return map[string]MetadataValue{}, false + } + return o.CustomProperties, true +} + +// HasCustomProperties returns a boolean if a field has been set. +func (o *McpTool) HasCustomProperties() bool { + if o != nil && !IsNil(o.CustomProperties) { + return true + } + + return false +} + +// SetCustomProperties gets a reference to the given map[string]MetadataValue and assigns it to the CustomProperties field. +func (o *McpTool) SetCustomProperties(v map[string]MetadataValue) { + o.CustomProperties = v +} + +func (o McpTool) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpTool) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["name"] = o.Name + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + toSerialize["accessType"] = o.AccessType + if !IsNil(o.Parameters) { + toSerialize["parameters"] = o.Parameters + } + if !IsNil(o.Revoked) { + toSerialize["revoked"] = o.Revoked + } + if !IsNil(o.RevokedReason) { + toSerialize["revokedReason"] = o.RevokedReason + } + if !IsNil(o.CustomProperties) { + toSerialize["customProperties"] = o.CustomProperties + } + return toSerialize, nil +} + +type NullableMcpTool struct { + value *McpTool + isSet bool +} + +func (v NullableMcpTool) Get() *McpTool { + return v.value +} + +func (v *NullableMcpTool) Set(val *McpTool) { + v.value = val + v.isSet = true +} + +func (v NullableMcpTool) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpTool) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpTool(val *McpTool) *NullableMcpTool { + return &NullableMcpTool{value: val, isSet: true} +} + +func (v NullableMcpTool) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpTool) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_tool_access_type.go b/catalog/pkg/openapi/model_mcp_tool_access_type.go new file mode 100644 index 0000000000..77e87f4003 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_tool_access_type.go @@ -0,0 +1,112 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "fmt" +) + +// McpToolAccessType Access type indicating what kind of operations the tool performs. - `read_only`: Tool only reads data - `read_write`: Tool can read and write data - `execute`: Tool executes operations +type McpToolAccessType string + +// List of McpToolAccessType +const ( + MCPTOOLACCESSTYPE_READ_ONLY McpToolAccessType = "read_only" + MCPTOOLACCESSTYPE_READ_WRITE McpToolAccessType = "read_write" + MCPTOOLACCESSTYPE_EXECUTE McpToolAccessType = "execute" +) + +// All allowed values of McpToolAccessType enum +var AllowedMcpToolAccessTypeEnumValues = []McpToolAccessType{ + "read_only", + "read_write", + "execute", +} + +func (v *McpToolAccessType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := McpToolAccessType(value) + for _, existing := range AllowedMcpToolAccessTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid McpToolAccessType", value) +} + +// NewMcpToolAccessTypeFromValue returns a pointer to a valid McpToolAccessType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewMcpToolAccessTypeFromValue(v string) (*McpToolAccessType, error) { + ev := McpToolAccessType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for McpToolAccessType: valid values are %v", v, AllowedMcpToolAccessTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v McpToolAccessType) IsValid() bool { + for _, existing := range AllowedMcpToolAccessTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to McpToolAccessType value +func (v McpToolAccessType) Ptr() *McpToolAccessType { + return &v +} + +type NullableMcpToolAccessType struct { + value *McpToolAccessType + isSet bool +} + +func (v NullableMcpToolAccessType) Get() *McpToolAccessType { + return v.value +} + +func (v *NullableMcpToolAccessType) Set(val *McpToolAccessType) { + v.value = val + v.isSet = true +} + +func (v NullableMcpToolAccessType) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpToolAccessType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpToolAccessType(val *McpToolAccessType) *NullableMcpToolAccessType { + return &NullableMcpToolAccessType{value: val, isSet: true} +} + +func (v NullableMcpToolAccessType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpToolAccessType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_tool_parameter.go b/catalog/pkg/openapi/model_mcp_tool_parameter.go new file mode 100644 index 0000000000..a13592bba9 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_tool_parameter.go @@ -0,0 +1,211 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the McpToolParameter type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &McpToolParameter{} + +// McpToolParameter A parameter for an MCP tool. +type McpToolParameter struct { + // Name of the parameter. + Name string `json:"name"` + // Data type of the parameter. + Type string `json:"type"` + // Description of the parameter. + Description *string `json:"description,omitempty"` + // Whether the parameter is required. + Required bool `json:"required"` +} + +type _McpToolParameter McpToolParameter + +// NewMcpToolParameter instantiates a new McpToolParameter object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMcpToolParameter(name string, type_ string, required bool) *McpToolParameter { + this := McpToolParameter{} + this.Name = name + this.Type = type_ + this.Required = required + return &this +} + +// NewMcpToolParameterWithDefaults instantiates a new McpToolParameter object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMcpToolParameterWithDefaults() *McpToolParameter { + this := McpToolParameter{} + return &this +} + +// GetName returns the Name field value +func (o *McpToolParameter) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *McpToolParameter) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *McpToolParameter) SetName(v string) { + o.Name = v +} + +// GetType returns the Type field value +func (o *McpToolParameter) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *McpToolParameter) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *McpToolParameter) SetType(v string) { + o.Type = v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *McpToolParameter) GetDescription() string { + if o == nil || IsNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *McpToolParameter) GetDescriptionOk() (*string, bool) { + if o == nil || IsNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *McpToolParameter) HasDescription() bool { + if o != nil && !IsNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *McpToolParameter) SetDescription(v string) { + o.Description = &v +} + +// GetRequired returns the Required field value +func (o *McpToolParameter) GetRequired() bool { + if o == nil { + var ret bool + return ret + } + + return o.Required +} + +// GetRequiredOk returns a tuple with the Required field value +// and a boolean to check if the value has been set. +func (o *McpToolParameter) GetRequiredOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Required, true +} + +// SetRequired sets field value +func (o *McpToolParameter) SetRequired(v bool) { + o.Required = v +} + +func (o McpToolParameter) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o McpToolParameter) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["name"] = o.Name + toSerialize["type"] = o.Type + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + toSerialize["required"] = o.Required + return toSerialize, nil +} + +type NullableMcpToolParameter struct { + value *McpToolParameter + isSet bool +} + +func (v NullableMcpToolParameter) Get() *McpToolParameter { + return v.value +} + +func (v *NullableMcpToolParameter) Set(val *McpToolParameter) { + v.value = val + v.isSet = true +} + +func (v NullableMcpToolParameter) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpToolParameter) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpToolParameter(val *McpToolParameter) *NullableMcpToolParameter { + return &NullableMcpToolParameter{value: val, isSet: true} +} + +func (v NullableMcpToolParameter) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpToolParameter) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_mcp_transport_type.go b/catalog/pkg/openapi/model_mcp_transport_type.go new file mode 100644 index 0000000000..db8653f780 --- /dev/null +++ b/catalog/pkg/openapi/model_mcp_transport_type.go @@ -0,0 +1,112 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "fmt" +) + +// McpTransportType Transport protocol used by the MCP server. - `stdio`: Standard input/output streams - `sse`: Server-Sent Events over HTTP - `http`: Standard HTTP/REST +type McpTransportType string + +// List of McpTransportType +const ( + MCPTRANSPORTTYPE_STDIO McpTransportType = "stdio" + MCPTRANSPORTTYPE_SSE McpTransportType = "sse" + MCPTRANSPORTTYPE_HTTP McpTransportType = "http" +) + +// All allowed values of McpTransportType enum +var AllowedMcpTransportTypeEnumValues = []McpTransportType{ + "stdio", + "sse", + "http", +} + +func (v *McpTransportType) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := McpTransportType(value) + for _, existing := range AllowedMcpTransportTypeEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid McpTransportType", value) +} + +// NewMcpTransportTypeFromValue returns a pointer to a valid McpTransportType +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewMcpTransportTypeFromValue(v string) (*McpTransportType, error) { + ev := McpTransportType(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for McpTransportType: valid values are %v", v, AllowedMcpTransportTypeEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v McpTransportType) IsValid() bool { + for _, existing := range AllowedMcpTransportTypeEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to McpTransportType value +func (v McpTransportType) Ptr() *McpTransportType { + return &v +} + +type NullableMcpTransportType struct { + value *McpTransportType + isSet bool +} + +func (v NullableMcpTransportType) Get() *McpTransportType { + return v.value +} + +func (v *NullableMcpTransportType) Set(val *McpTransportType) { + v.value = val + v.isSet = true +} + +func (v NullableMcpTransportType) IsSet() bool { + return v.isSet +} + +func (v *NullableMcpTransportType) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMcpTransportType(val *McpTransportType) *NullableMcpTransportType { + return &NullableMcpTransportType{value: val, isSet: true} +} + +func (v NullableMcpTransportType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMcpTransportType) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/scripts/gen_openapi_server.sh b/catalog/scripts/gen_openapi_server.sh index 0745a17621..c25e23881b 100755 --- a/catalog/scripts/gen_openapi_server.sh +++ b/catalog/scripts/gen_openapi_server.sh @@ -35,6 +35,7 @@ py-re-replace 0 'model\.\[\]ArtifactType2QueryParam' '[]model.ArtifactTypeQueryP py-re-replace 1 'github\.com/kubeflow/model-registry/pkg/openapi' 'github.com/kubeflow/model-registry/catalog/pkg/openapi' \ "$PROJECT_ROOT"/internal/server/openapi/api_model_catalog_service.go \ + "$PROJECT_ROOT"/internal/server/openapi/api_mcp_catalog_service.go \ "$PROJECT_ROOT"/internal/server/openapi/api.go py-re-replace 1 '\{model_name\+\}|model_name\+' '*' "$PROJECT_ROOT"/internal/server/openapi/api_model_catalog_service.go diff --git a/catalog/scripts/gen_type_asserts.py b/catalog/scripts/gen_type_asserts.py index 9491a5a829..28b1a4010c 100644 --- a/catalog/scripts/gen_type_asserts.py +++ b/catalog/scripts/gen_type_asserts.py @@ -1,7 +1,38 @@ import typing as t +import re from pathlib import Path from textwrap import dedent +# Fields that are pointers in the model but referenced without dereferencing in assertions +# Format: (field_name, assertion_func_name) +POINTER_FIELD_FIXES = [ + # McpServer fields that are pointers + ('obj.SecurityIndicators', 'AssertMcpSecurityIndicatorConstraints'), + ('obj.SecurityIndicators', 'AssertMcpSecurityIndicatorRequired'), + ('obj.Endpoints', 'AssertMcpEndpointsConstraints'), + ('obj.Endpoints', 'AssertMcpEndpointsRequired'), +] + +def fix_pointer_dereferences(func: str) -> str: + """Fix pointer dereference issues in assertion functions. + + When a field is a pointer type but passed to a function expecting a non-pointer, + we need to wrap it with nil check and dereference. + """ + result = func + + for field_name, func_name in POINTER_FIELD_FIXES: + # Find patterns like: + # \tif err := AssertMcpSecurityIndicatorConstraints(obj.SecurityIndicators); err != nil { + # \t\treturn err + # \t} + old_pattern = f'\tif err := {func_name}({field_name}); err != nil {{\n\t\treturn err\n\t}}' + new_pattern = f'\tif {field_name} != nil {{\n\t\tif err := {func_name}(*{field_name}); err != nil {{\n\t\t\treturn err\n\t\t}}\n\t}}' + + result = result.replace(old_pattern, new_pattern) + + return result + def get_funcs(models: t.Iterable[Path]) -> t.Iterator[str]: for path in models: with path.open() as f: @@ -31,7 +62,10 @@ def get_funcs(models: t.Iterable[Path]) -> t.Iterator[str]: in_func = True elif line.startswith("}") and in_func: in_func = False - yield "\n".join(buf) + func_str = "\n".join(buf) + # Apply pointer dereference fixes + func_str = fix_pointer_dereferences(func_str) + yield func_str buf.clear() path.unlink() diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index a675c0687a..f688692891 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -74,6 +74,14 @@ const ( ModelCatalogSettingsSourceConfigListPath = ModelCatalogSettingsPathPrefix + "/source_configs" ModelCatalogSettingsSourceConfigPath = ModelCatalogSettingsSourceConfigListPath + "/:" + CatalogSourceId CatalogSourcePreviewPath = ModelCatalogSettingsPathPrefix + "/source_preview" + + // MCP catalog + McpServerId = "server_id" + McpCatalogPathPrefix = ApiPathPrefix + "/mcp_catalog" + McpServerListPath = McpCatalogPathPrefix + "/mcp_servers" + McpServerPath = McpServerListPath + "/server/:" + McpServerId + McpFilterOptionsListPath = McpServerListPath + "/filter_options" + McpSourceListPath = McpCatalogPathPrefix + "/sources" ) type App struct { @@ -238,6 +246,14 @@ func (app *App) Routes() http.Handler { apiRouter.GET(CatalogSourceModelCatchAllPath, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetCatalogSourceModelHandler))) apiRouter.GET(CatalogSourceModelArtifactsCatchAll, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetCatalogSourceModelArtifactsHandler))) apiRouter.GET(CatalogModelPerformanceArtifacts, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetCatalogModelPerformanceArtifactsHandler))) + + // MCP catalog routes + // Note: static paths (filter_options) must be registered before wildcard paths (:server_id) + apiRouter.GET(McpFilterOptionsListPath, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetMcpFilterOptionsHandler))) + apiRouter.GET(McpServerListPath, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetAllMcpServersHandler))) + apiRouter.GET(McpServerPath, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetMcpServerHandler))) + apiRouter.GET(McpSourceListPath, app.AttachNamespace(app.AttachModelCatalogRESTClient(app.GetAllMcpSourcesHandler))) + // Kubernetes routes apiRouter.GET(UserPath, app.UserHandler) apiRouter.GET(ModelRegistryListPath, app.AttachNamespace(app.RequireListServiceAccessInNamespace(app.GetAllModelRegistriesHandler))) diff --git a/clients/ui/bff/internal/api/mcp_server_handler.go b/clients/ui/bff/internal/api/mcp_server_handler.go new file mode 100644 index 0000000000..cd8edac72d --- /dev/null +++ b/clients/ui/bff/internal/api/mcp_server_handler.go @@ -0,0 +1,130 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" + "github.com/kubeflow/model-registry/ui/bff/internal/integrations/httpclient" + "github.com/kubeflow/model-registry/ui/bff/internal/models" +) + +// McpServerListEnvelope wraps the MCP server list response +type McpServerListEnvelope Envelope[*models.McpServerList, None] + +// McpServerEnvelope wraps a single MCP server response +type McpServerEnvelope Envelope[*models.McpServer, None] + +// McpCatalogSourceListEnvelope wraps the MCP catalog source list response +type McpCatalogSourceListEnvelope Envelope[*models.McpCatalogSourceList, None] + +// McpFilterOptionsListEnvelope wraps the MCP filter options response +type McpFilterOptionsListEnvelope Envelope[*models.FilterOptionsList, None] + +// GetAllMcpServersHandler handles GET /api/v1/mcp_catalog/mcp_servers +func (app *App) GetAllMcpServersHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + client, ok := r.Context().Value(constants.ModelCatalogHttpClientKey).(httpclient.HTTPClientInterface) + if !ok { + app.serverErrorResponse(w, r, errors.New("catalog REST client not found")) + return + } + + mcpServers, err := app.repositories.ModelCatalogClient.GetAllMcpServers(client, r.URL.Query()) + + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + serverList := McpServerListEnvelope{ + Data: mcpServers, + } + + err = app.WriteJSON(w, http.StatusOK, serverList, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} + +// GetMcpServerHandler handles GET /api/v1/mcp_catalog/mcp_servers/:server_id +func (app *App) GetMcpServerHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + client, ok := r.Context().Value(constants.ModelCatalogHttpClientKey).(httpclient.HTTPClientInterface) + if !ok { + app.serverErrorResponse(w, r, errors.New("catalog REST client not found")) + return + } + + serverId := ps.ByName(McpServerId) + + mcpServer, err := app.repositories.ModelCatalogClient.GetMcpServer(client, serverId) + + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + if mcpServer == nil { + app.notFoundResponse(w, r) + return + } + + serverEnvelope := McpServerEnvelope{ + Data: mcpServer, + } + + err = app.WriteJSON(w, http.StatusOK, serverEnvelope, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} + +// GetMcpFilterOptionsHandler handles GET /api/v1/mcp_catalog/filter_options +func (app *App) GetMcpFilterOptionsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + client, ok := r.Context().Value(constants.ModelCatalogHttpClientKey).(httpclient.HTTPClientInterface) + if !ok { + app.serverErrorResponse(w, r, errors.New("catalog REST client not found")) + return + } + + filterOptions, err := app.repositories.ModelCatalogClient.GetMcpFilterOptions(client) + + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + response := McpFilterOptionsListEnvelope{ + Data: filterOptions, + } + + err = app.WriteJSON(w, http.StatusOK, response, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} + +// GetAllMcpSourcesHandler handles GET /api/v1/mcp_catalog/sources +func (app *App) GetAllMcpSourcesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + client, ok := r.Context().Value(constants.ModelCatalogHttpClientKey).(httpclient.HTTPClientInterface) + if !ok { + app.serverErrorResponse(w, r, errors.New("catalog REST client not found")) + return + } + + mcpSources, err := app.repositories.ModelCatalogClient.GetAllMcpSources(client, r.URL.Query()) + + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + sourcesList := McpCatalogSourceListEnvelope{ + Data: mcpSources, + } + + err = app.WriteJSON(w, http.StatusOK, sourcesList, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/clients/ui/bff/internal/mocks/model_catalog_client_mock.go b/clients/ui/bff/internal/mocks/model_catalog_client_mock.go index 60f866281b..471ba46dc6 100644 --- a/clients/ui/bff/internal/mocks/model_catalog_client_mock.go +++ b/clients/ui/bff/internal/mocks/model_catalog_client_mock.go @@ -219,3 +219,94 @@ func (m *ModelCatalogClientMock) CreateCatalogSourcePreview(client httpclient.HT return &catalogSourcePreview, nil } + +func (m *ModelCatalogClientMock) GetAllMcpServers(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpServerList, error) { + allServers := GetMcpServerMocks() + + pageSizeStr := pageValues.Get("pageSize") + pageSize := 10 // default + if pageSizeStr != "" { + if parsed, err := strconv.Atoi(pageSizeStr); err == nil && parsed > 0 { + pageSize = parsed + } + } + + pageTokenStr := pageValues.Get("nextPageToken") + startIndex := 0 + if pageTokenStr != "" { + if parsed, err := strconv.Atoi(pageTokenStr); err == nil && parsed > 0 { + startIndex = parsed + } + } + + totalSize := len(allServers) + endIndex := startIndex + pageSize + if endIndex > totalSize { + endIndex = totalSize + } + + var pagedServers []models.McpServer + if startIndex < totalSize { + pagedServers = allServers[startIndex:endIndex] + } else { + pagedServers = []models.McpServer{} + } + + var nextPageToken string + if endIndex < totalSize { + nextPageToken = strconv.Itoa(endIndex) + } + + size := len(pagedServers) + if size > math.MaxInt32 { + size = math.MaxInt32 + } + ps := pageSize + if ps > math.MaxInt32 { + ps = math.MaxInt32 + } + + mcpServerList := models.McpServerList{ + Items: pagedServers, + Size: int32(size), + PageSize: int32(ps), + NextPageToken: nextPageToken, + } + + return &mcpServerList, nil +} + +func (m *ModelCatalogClientMock) GetMcpServer(client httpclient.HTTPClientInterface, serverId string) (*models.McpServer, error) { + allServers := GetMcpServerMocks() + + for _, server := range allServers { + if server.ID == serverId { + return &server, nil + } + } + + return nil, fmt.Errorf("MCP server not found for serverId: %s", serverId) +} + +func (m *ModelCatalogClientMock) GetMcpFilterOptions(client httpclient.HTTPClientInterface) (*models.FilterOptionsList, error) { + filterOptions := GetMcpFilterOptionsListMock() + return &filterOptions, nil +} + +func (m *ModelCatalogClientMock) GetAllMcpSources(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpCatalogSourceList, error) { + allSources := GetMcpCatalogSourceMocks() + + size := len(allSources) + if size > math.MaxInt32 { + size = math.MaxInt32 + } + + mcpSourceList := models.McpCatalogSourceList{ + Items: allSources, + Size: int32(size), + PageSize: int32(10), + NextPageToken: "", + } + + return &mcpSourceList, nil +} diff --git a/clients/ui/bff/internal/mocks/static_data_mock.go b/clients/ui/bff/internal/mocks/static_data_mock.go index b55d887f6e..9407152d49 100644 --- a/clients/ui/bff/internal/mocks/static_data_mock.go +++ b/clients/ui/bff/internal/mocks/static_data_mock.go @@ -1357,6 +1357,35 @@ func GetFilterOptionsListMock() models.FilterOptionsList { } } +func GetMcpFilterOptionsListMock() models.FilterOptionsList { + filterOptions := map[string]models.FilterOption{ + "provider": { + Type: "string", + Values: []interface{}{"Anthropic", "OpenAI"}, + }, + "license": { + Type: "string", + Values: []interface{}{"Apache 2.0", "MIT"}, + }, + "tags": { + Type: "string", + Values: []interface{}{"ai", "code-assistant", "llm"}, + }, + "transports": { + Type: "string", + Values: []interface{}{"http", "sse", "stdio"}, + }, + "deploymentMode": { + Type: "string", + Values: []interface{}{"local", "remote"}, + }, + } + + return models.FilterOptionsList{ + Filters: &filterOptions, + } +} + func CreateSampleCatalogSource(id string, name string, catalogType string, enabled bool) models.CatalogSourceConfig { defaultCatalog := id == "sample-source" @@ -1499,3 +1528,87 @@ func CreateCatalogSourcePreviewMock() models.CatalogSourcePreviewResult { Size: int32(len(catalogModelPreview)), } } + +func GetMcpServerMocks() []models.McpServer { + return []models.McpServer{ + { + ID: "filesystem-server", + Name: "Filesystem MCP Server", + Description: "Provides secure file system operations with configurable access controls", + SourceId: stringToPointer("organization_mcp_servers"), + Provider: stringToPointer("Anthropic"), + Transports: []models.McpTransportType{models.McpTransportTypeStdio}, + Tags: []string{"filesystem", "io", "storage"}, + Tools: []models.McpTool{ + { + Name: "read_file", + Description: "Read the complete contents of a file from the file system", + }, + { + Name: "write_file", + Description: "Create a new file or completely overwrite an existing file", + }, + { + Name: "list_directory", + Description: "Get a detailed listing of all files and directories in a specified path", + }, + }, + }, + { + ID: "github-server", + Name: "GitHub MCP Server", + Description: "Interact with GitHub repositories, issues, pull requests, and more", + SourceId: stringToPointer("organization_mcp_servers"), + Provider: stringToPointer("Anthropic"), + Transports: []models.McpTransportType{models.McpTransportTypeStdio}, + Tags: []string{"git", "version-control", "ci-cd"}, + Tools: []models.McpTool{ + { + Name: "create_repository", + Description: "Create a new GitHub repository in your account or organization", + }, + { + Name: "search_repositories", + Description: "Search for GitHub repositories", + }, + }, + }, + { + ID: "slack-server", + Name: "Slack MCP Server", + Description: "Interact with Slack workspaces to send messages, manage channels, and more", + SourceId: stringToPointer("community_mcp_servers"), + Provider: stringToPointer("Anthropic"), + Transports: []models.McpTransportType{models.McpTransportTypeSSE}, + Tags: []string{"messaging", "collaboration", "notifications"}, + Tools: []models.McpTool{ + { + Name: "send_message", + Description: "Send a message to a Slack channel", + }, + }, + }, + } +} + +func GetMcpCatalogSourceMocks() []models.McpCatalogSource { + enabled := true + availableStatus := models.McpCatalogSourceStatusAvailable + + return []models.McpCatalogSource{ + { + ID: "organization_mcp_servers", + Name: "Organization MCP Servers", + Labels: []string{"Organization MCP"}, + Enabled: &enabled, + Status: &availableStatus, + }, + { + ID: "community_mcp_servers", + Name: "Community and Custom MCP Servers", + Labels: []string{"Community and custom"}, + Enabled: &enabled, + Status: &availableStatus, + }, + } +} diff --git a/clients/ui/bff/internal/models/mcp_server.go b/clients/ui/bff/internal/models/mcp_server.go new file mode 100644 index 0000000000..720c88c16d --- /dev/null +++ b/clients/ui/bff/internal/models/mcp_server.go @@ -0,0 +1,155 @@ +package models + +// McpToolAccessType represents the access type for an MCP tool +type McpToolAccessType string + +const ( + McpToolAccessTypeReadOnly McpToolAccessType = "read_only" + McpToolAccessTypeReadWrite McpToolAccessType = "read_write" + McpToolAccessTypeExecute McpToolAccessType = "execute" +) + +// McpTransportType represents the transport protocol for an MCP server +type McpTransportType string + +const ( + McpTransportTypeStdio McpTransportType = "stdio" + McpTransportTypeSSE McpTransportType = "sse" + McpTransportTypeHTTP McpTransportType = "http" +) + +// McpDeploymentMode represents the deployment mode for an MCP server +type McpDeploymentMode string + +const ( + McpDeploymentModeLocal McpDeploymentMode = "local" + McpDeploymentModeRemote McpDeploymentMode = "remote" +) + +// McpEndpoints represents network endpoints for remote MCP servers +type McpEndpoints struct { + Http *string `json:"http,omitempty"` + Sse *string `json:"sse,omitempty"` +} + +// McpArtifact represents an artifact for an MCP server (e.g., OCI image) +type McpArtifact struct { + Uri string `json:"uri"` + CreateTimeSinceEpoch *string `json:"createTimeSinceEpoch,omitempty"` + LastUpdateTimeSinceEpoch *string `json:"lastUpdateTimeSinceEpoch,omitempty"` +} + +// McpSecurityIndicator represents security indicators for an MCP server +type McpSecurityIndicator struct { + VerifiedSource bool `json:"verifiedSource"` + SecureEndpoint bool `json:"secureEndpoint"` + Sast bool `json:"sast"` + ReadOnlyTools bool `json:"readOnlyTools"` +} + +// McpToolParameter represents a parameter for an MCP tool +type McpToolParameter struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` +} + +// McpMetadataStringValue represents a string custom property value +type McpMetadataStringValue struct { + StringValue string `json:"string_value"` + MetadataType string `json:"metadataType"` +} + +// McpMetadataBoolValue represents a boolean custom property value +type McpMetadataBoolValue struct { + BoolValue bool `json:"bool_value"` + MetadataType string `json:"metadataType"` +} + +// McpCustomProperties represents custom properties following Model Registry patterns. +// Tags are stored as MetadataStringValue entries with empty string_value (label pattern). +// Security indicators are stored as MetadataBoolValue entries. +type McpCustomProperties map[string]interface{} + +// McpTool represents a tool exposed by an MCP server +type McpTool struct { + Name string `json:"name"` + Description string `json:"description"` + AccessType McpToolAccessType `json:"accessType"` + Parameters []McpToolParameter `json:"parameters,omitempty"` + Revoked *bool `json:"revoked,omitempty"` + RevokedReason *string `json:"revokedReason,omitempty"` + CustomProperties *McpCustomProperties `json:"customProperties,omitempty"` +} + +// McpServer represents an MCP server in the catalog +type McpServer struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + SourceId *string `json:"source_id,omitempty"` + Logo *string `json:"logo,omitempty"` + License *string `json:"license,omitempty"` + LicenseLink *string `json:"license_link,omitempty"` + Provider *string `json:"provider,omitempty"` + Version *string `json:"version,omitempty"` + Tags []string `json:"tags,omitempty"` + Tools []McpTool `json:"tools,omitempty"` + SecurityIndicators *McpSecurityIndicator `json:"securityIndicators,omitempty"` + DocumentationUrl *string `json:"documentationUrl,omitempty"` + RepositoryUrl *string `json:"repositoryUrl,omitempty"` + SourceCode *string `json:"sourceCode,omitempty"` + LastUpdated *string `json:"lastUpdated,omitempty"` + PublishedDate *string `json:"publishedDate,omitempty"` + Artifacts []McpArtifact `json:"artifacts,omitempty"` + Transports []McpTransportType `json:"transports,omitempty"` + Readme *string `json:"readme,omitempty"` + DeploymentMode *McpDeploymentMode `json:"deploymentMode,omitempty"` + Endpoints *McpEndpoints `json:"endpoints,omitempty"` + CustomProperties *McpCustomProperties `json:"customProperties,omitempty"` +} + +// McpServerList represents a paginated list of MCP servers +type McpServerList struct { + NextPageToken string `json:"nextPageToken"` + PageSize int32 `json:"pageSize"` + Size int32 `json:"size"` + Items []McpServer `json:"items"` +} + +// McpCatalogSourceStatus represents the status of an MCP catalog source +type McpCatalogSourceStatus string + +const ( + McpCatalogSourceStatusAvailable McpCatalogSourceStatus = "available" + McpCatalogSourceStatusError McpCatalogSourceStatus = "error" + McpCatalogSourceStatusDisabled McpCatalogSourceStatus = "disabled" +) + +// CatalogAssetType represents the type of assets in a catalog source +type CatalogAssetType string + +const ( + CatalogAssetTypeModels CatalogAssetType = "models" + CatalogAssetTypeMcpServers CatalogAssetType = "mcp_servers" +) + +// McpCatalogSource represents a source of MCP servers in the catalog +type McpCatalogSource struct { + ID string `json:"id"` + Name string `json:"name"` + Labels []string `json:"labels"` + Enabled *bool `json:"enabled,omitempty"` + AssetType *CatalogAssetType `json:"assetType,omitempty"` + Status *McpCatalogSourceStatus `json:"status,omitempty"` + Error *string `json:"error,omitempty"` +} + +// McpCatalogSourceList represents a paginated list of MCP catalog sources +type McpCatalogSourceList struct { + NextPageToken string `json:"nextPageToken"` + PageSize int32 `json:"pageSize"` + Size int32 `json:"size"` + Items []McpCatalogSource `json:"items"` +} diff --git a/clients/ui/bff/internal/repositories/helpers.go b/clients/ui/bff/internal/repositories/helpers.go index cc21f9b4f4..fbcaa1a7b3 100644 --- a/clients/ui/bff/internal/repositories/helpers.go +++ b/clients/ui/bff/internal/repositories/helpers.go @@ -62,6 +62,9 @@ func FilterPageValues(values url.Values) url.Values { if v := values.Get("filterStatus"); v != "" { result.Set("filterStatus", v) } + if v := values.Get("assetType"); v != "" { + result.Set("assetType", v) + } return result } diff --git a/clients/ui/bff/internal/repositories/mcp_servers.go b/clients/ui/bff/internal/repositories/mcp_servers.go new file mode 100644 index 0000000000..414326d2c4 --- /dev/null +++ b/clients/ui/bff/internal/repositories/mcp_servers.go @@ -0,0 +1,102 @@ +package repositories + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/kubeflow/model-registry/ui/bff/internal/integrations/httpclient" + "github.com/kubeflow/model-registry/ui/bff/internal/models" +) + +const mcpServersPath = "/mcp_servers" +const mcpFilterOptionsPath = "/mcp_servers/filter_options" + +// McpServersInterface defines operations for MCP server catalog access via catalog service +type McpServersInterface interface { + GetAllMcpServers(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpServerList, error) + GetMcpServer(client httpclient.HTTPClientInterface, serverId string) (*models.McpServer, error) + GetMcpFilterOptions(client httpclient.HTTPClientInterface) (*models.FilterOptionsList, error) + GetAllMcpSources(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpCatalogSourceList, error) +} + +// McpServers implements McpServersInterface +type McpServers struct { + McpServersInterface +} + +// GetAllMcpServers fetches all MCP servers from the catalog service +func (m McpServers) GetAllMcpServers(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpServerList, error) { + responseData, err := client.GET(UrlWithPageParams(mcpServersPath, pageValues)) + if err != nil { + return nil, fmt.Errorf("error fetching MCP servers: %w", err) + } + + var serverList models.McpServerList + + if err := json.Unmarshal(responseData, &serverList); err != nil { + return nil, fmt.Errorf("error decoding MCP servers response: %w", err) + } + + return &serverList, nil +} + +// GetMcpServer fetches a specific MCP server by ID from the catalog service +func (m McpServers) GetMcpServer(client httpclient.HTTPClientInterface, serverId string) (*models.McpServer, error) { + path, err := url.JoinPath(mcpServersPath, serverId) + if err != nil { + return nil, err + } + + responseData, err := client.GET(path) + if err != nil { + return nil, fmt.Errorf("error fetching MCP server: %w", err) + } + + var server models.McpServer + + if err := json.Unmarshal(responseData, &server); err != nil { + return nil, fmt.Errorf("error decoding MCP server response: %w", err) + } + + return &server, nil +} + +// GetMcpFilterOptions fetches filter options for MCP servers from the catalog service +func (m McpServers) GetMcpFilterOptions(client httpclient.HTTPClientInterface) (*models.FilterOptionsList, error) { + responseData, err := client.GET(mcpFilterOptionsPath) + if err != nil { + return nil, fmt.Errorf("error fetching MCP filter options: %w", err) + } + + var filterOptions models.FilterOptionsList + + if err := json.Unmarshal(responseData, &filterOptions); err != nil { + return nil, fmt.Errorf("error decoding MCP filter options response: %w", err) + } + + return &filterOptions, nil +} + +// GetAllMcpSources fetches all MCP catalog sources from the catalog service +// It uses the unified /sources endpoint with assetType=mcp_servers filter +func (m McpServers) GetAllMcpSources(client httpclient.HTTPClientInterface, pageValues url.Values) (*models.McpCatalogSourceList, error) { + // Add assetType filter for MCP servers + if pageValues == nil { + pageValues = url.Values{} + } + pageValues.Set("assetType", "mcp_servers") + + responseData, err := client.GET(UrlWithPageParams(sourcesPath, pageValues)) + if err != nil { + return nil, fmt.Errorf("error fetching MCP sources: %w", err) + } + + var sourceList models.McpCatalogSourceList + + if err := json.Unmarshal(responseData, &sourceList); err != nil { + return nil, fmt.Errorf("error decoding MCP sources response: %w", err) + } + + return &sourceList, nil +} diff --git a/clients/ui/bff/internal/repositories/model_catalog_client.go b/clients/ui/bff/internal/repositories/model_catalog_client.go index bab728abc9..101aca7f3e 100644 --- a/clients/ui/bff/internal/repositories/model_catalog_client.go +++ b/clients/ui/bff/internal/repositories/model_catalog_client.go @@ -8,6 +8,7 @@ type ModelCatalogClientInterface interface { CatalogSourcesInterface CatalogModelsInterface CatalogSourcePreviewInterface + McpServersInterface } type ModelCatalogClient struct { @@ -15,6 +16,7 @@ type ModelCatalogClient struct { CatalogSources CatalogModels CatalogSourcePreview + McpServers } func NewModelCatalogClient(logger *slog.Logger) (ModelCatalogClientInterface, error) { diff --git a/clients/ui/frontend/src/app/AppRoutes.tsx b/clients/ui/frontend/src/app/AppRoutes.tsx index bbd606d434..fd81e9e3a2 100644 --- a/clients/ui/frontend/src/app/AppRoutes.tsx +++ b/clients/ui/frontend/src/app/AppRoutes.tsx @@ -7,7 +7,9 @@ import ModelRegistrySettingsRoutes from './pages/settings/ModelRegistrySettingsR import ModelRegistryRoutes from './pages/modelRegistry/ModelRegistryRoutes'; import ModelCatalogRoutes from './pages/modelCatalog/ModelCatalogRoutes'; import ModelCatalogSettingsRoutes from './pages/modelCatalogSettings/ModelCatalogSettingsRoutes'; +import McpCatalogRoutes from './pages/mcpCatalog/McpCatalogRoutes'; import { modelCatalogUrl } from './routes/modelCatalog/catalogModel'; +import { mcpCatalogUrl } from './routes/mcpCatalog/mcpCatalog'; import { catalogSettingsUrl, CATALOG_SETTINGS_PAGE_TITLE, @@ -53,12 +55,16 @@ export const useNavData = (): NavDataItem[] => { }, ]; - // Only show Model Catalog in Standalone or Federated mode + // Only show Model Catalog and MCP Catalog in Standalone or Federated mode if (isStandalone || isFederated) { baseNavItems.push({ label: 'Model Catalog', path: modelCatalogUrl(), }); + baseNavItems.push({ + label: 'MCP Catalog', + path: mcpCatalogUrl(), + }); } return [...baseNavItems, ...useAdminSettings()]; @@ -78,6 +84,7 @@ const AppRoutes: React.FC = () => { {(isStandalone || isFederated) && ( <> } /> + } /> } /> > )} diff --git a/clients/ui/frontend/src/app/api/mcpCatalog/service.ts b/clients/ui/frontend/src/app/api/mcpCatalog/service.ts new file mode 100644 index 0000000000..4c225cf59b --- /dev/null +++ b/clients/ui/frontend/src/app/api/mcpCatalog/service.ts @@ -0,0 +1,90 @@ +import { APIOptions, handleRestFailures, isModArchResponse, restGET } from 'mod-arch-core'; +import { + McpCatalogSourceList, + McpFilterOptionsList, + McpServer, + McpServerList, +} from '~/app/pages/mcpCatalog/types'; + +/** + * Get all MCP servers from the catalog + * @param hostPath - The base URL for the MCP catalog API + * @param queryParams - Additional query parameters to include in the request + * @returns A function that fetches MCP servers with optional filters + */ +export const getMcpServers = + (hostPath: string, queryParams: Record = {}) => + ( + opts: APIOptions, + sourceLabel?: string, + pageSize?: number, + filterQuery?: string, + searchTerm?: string, + ): Promise => { + const params = { ...queryParams }; + if (sourceLabel) { + params.sourceLabel = sourceLabel; + } + if (pageSize) { + params.pageSize = pageSize; + } + if (filterQuery) { + params.filterQuery = filterQuery; + } + if (searchTerm) { + params.q = searchTerm; + } + return handleRestFailures(restGET(hostPath, '/mcp_servers', params, opts)).then((response) => { + if (isModArchResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }); + }; + +/** + * Get a specific MCP server by ID + */ +export const getMcpServer = + (hostPath: string, queryParams: Record = {}) => + (opts: APIOptions, serverId: string): Promise => + handleRestFailures( + restGET(hostPath, `/mcp_servers/server/${serverId}`, queryParams, opts), + ).then((response) => { + if (isModArchResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }); + +/** + * Get all MCP catalog sources + * Filters by assetType=mcp_servers to only return MCP server sources + */ +export const getMcpSources = + (hostPath: string, queryParams: Record = {}) => + (opts: APIOptions): Promise => + handleRestFailures( + restGET(hostPath, '/sources', { ...queryParams, assetType: 'mcp_servers' }, opts), + ).then((response) => { + if (isModArchResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }); + +/** + * Get filter options for MCP servers + * Returns available values for each filterable field + */ +export const getMcpFilterOptions = + (hostPath: string, queryParams: Record = {}) => + (opts: APIOptions): Promise => + handleRestFailures(restGET(hostPath, '/mcp_servers/filter_options', queryParams, opts)).then( + (response) => { + if (isModArchResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }, + ); diff --git a/clients/ui/frontend/src/app/api/modelCatalog/service.ts b/clients/ui/frontend/src/app/api/modelCatalog/service.ts index 98a0aa752e..e708fcde66 100644 --- a/clients/ui/frontend/src/app/api/modelCatalog/service.ts +++ b/clients/ui/frontend/src/app/api/modelCatalog/service.ts @@ -57,7 +57,9 @@ export const getCatalogFilterOptionList = export const getListSources = (hostPath: string, queryParams: Record = {}) => (opts: APIOptions): Promise => - handleRestFailures(restGET(hostPath, '/sources', queryParams, opts)).then((response) => { + handleRestFailures( + restGET(hostPath, '/sources', { ...queryParams, assetType: 'models' }, opts), + ).then((response) => { if (isModArchResponse(response)) { return response.data; } diff --git a/clients/ui/frontend/src/app/context/mcpCatalog/McpCatalogContext.tsx b/clients/ui/frontend/src/app/context/mcpCatalog/McpCatalogContext.tsx new file mode 100644 index 0000000000..7c76738ba6 --- /dev/null +++ b/clients/ui/frontend/src/app/context/mcpCatalog/McpCatalogContext.tsx @@ -0,0 +1,232 @@ +import { useQueryParamNamespaces } from 'mod-arch-core'; +import * as React from 'react'; +import useMcpCatalogAPIState, { + McpCatalogAPIState, +} from '~/app/hooks/mcpCatalog/useMcpCatalogAPIState'; +import { useMcpFilterOptions } from '~/app/hooks/mcpCatalog/useMcpFilterOptions'; +import { useMcpServers } from '~/app/hooks/mcpCatalog/useMcpServers'; +import { useMcpSources } from '~/app/hooks/mcpCatalog/useMcpSources'; +import { + McpCategoryName, + McpCatalogSourceList, + McpFilterOptionsList, + McpServerList, +} from '~/app/pages/mcpCatalog/types'; +import { + McpServerFilterState, + mcpFiltersToFilterQuery, +} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import { BFF_API_VERSION, URL_PREFIX } from '~/app/utilities/const'; + +/** + * Filter options derived from backend filter_options endpoint. + * These remain stable regardless of applied filters. + */ +export type McpFilterOptions = { + providers: string[]; + licenses: string[]; + tags: string[]; + transports: string[]; + deploymentModes: string[]; +}; + +const EMPTY_FILTER_OPTIONS: McpFilterOptions = { + providers: [], + licenses: [], + tags: [], + transports: [], + deploymentModes: [], +}; + +/** + * Convert backend filter options response to the frontend McpFilterOptions type. + * The backend returns a map of field names to filter option objects with values arrays. + */ +const convertBackendFilterOptions = (backendOptions: McpFilterOptionsList): McpFilterOptions => { + const filters = backendOptions.filters ?? {}; + + const getStringValues = (key: string): string[] => { + if (!Object.prototype.hasOwnProperty.call(filters, key)) { + return []; + } + const option = filters[key]; + if (!option.values) { + return []; + } + return option.values.filter((v): v is string => typeof v === 'string').toSorted(); + }; + + return { + providers: getStringValues('provider'), + licenses: getStringValues('license'), + tags: getStringValues('tags'), + transports: getStringValues('transports'), + deploymentModes: getStringValues('deploymentMode'), + }; +}; + +// Initial empty filter state +const EMPTY_FILTER_STATE: McpServerFilterState = { + selectedProviders: [], + selectedLicenses: [], + selectedTags: [], + selectedTransports: [], + selectedDeploymentModes: [], +}; + +export type McpCatalogContextType = { + mcpServersLoaded: boolean; + mcpServersLoadError?: Error; + mcpServers: McpServerList | null; + mcpSources: McpCatalogSourceList | null; + mcpSourcesLoaded: boolean; + mcpSourcesLoadError?: Error; + selectedSourceLabel: string; + updateSelectedSourceLabel: (label: string) => void; + // Filter state management + filters: McpServerFilterState; + searchTerm: string; + updateFilters: (filters: McpServerFilterState) => void; + updateSearchTerm: (term: string) => void; + resetFilters: () => void; + // Filter options from backend filter_options endpoint + filterOptions: McpFilterOptions; + filterOptionsLoaded: boolean; + filterOptionsLoadError?: Error; + apiState: McpCatalogAPIState; + refreshAPIState: () => void; + refreshMcpServers: () => void; + refreshMcpSources: () => void; +}; + +type McpCatalogContextProviderProps = { + children: React.ReactNode; +}; + +export const McpCatalogContext = React.createContext({ + mcpServersLoaded: false, + mcpServersLoadError: undefined, + mcpServers: null, + mcpSources: null, + mcpSourcesLoaded: false, + mcpSourcesLoadError: undefined, + selectedSourceLabel: McpCategoryName.allServers, + updateSelectedSourceLabel: () => undefined, + filters: EMPTY_FILTER_STATE, + searchTerm: '', + updateFilters: () => undefined, + updateSearchTerm: () => undefined, + resetFilters: () => undefined, + filterOptions: EMPTY_FILTER_OPTIONS, + filterOptionsLoaded: false, + filterOptionsLoadError: undefined, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + apiState: { apiAvailable: false, api: null as unknown as McpCatalogAPIState['api'] }, + refreshAPIState: () => undefined, + refreshMcpServers: () => undefined, + refreshMcpSources: () => undefined, +}); + +export const McpCatalogContextProvider: React.FC = ({ + children, +}) => { + const hostPath = `${URL_PREFIX}/api/${BFF_API_VERSION}/mcp_catalog`; + const queryParams = useQueryParamNamespaces(); + const [apiState, refreshAPIState] = useMcpCatalogAPIState(hostPath, queryParams); + const [selectedSourceLabel, setSelectedSourceLabel] = React.useState( + McpCategoryName.allServers, + ); + + // Filter and search state + const [filters, setFilters] = React.useState(EMPTY_FILTER_STATE); + const [searchTerm, setSearchTerm] = React.useState(''); + + // Build filterQuery from current filter state + const filterQuery = React.useMemo(() => mcpFiltersToFilterQuery(filters), [filters]); + + // Fetch filter options from the dedicated backend endpoint + const [backendFilterOptions, filterOptionsLoaded, filterOptionsLoadError] = + useMcpFilterOptions(apiState); + + // Pass filterQuery and searchTerm to useMcpServers for the filtered results + const [mcpServers, mcpServersLoaded, mcpServersLoadError, refreshMcpServers] = useMcpServers( + apiState, + { filterQuery: filterQuery || undefined, searchTerm: searchTerm || undefined }, + ); + const [mcpSources, mcpSourcesLoaded, mcpSourcesLoadError, refreshMcpSources] = + useMcpSources(apiState); + + // Convert backend filter options to frontend format + const filterOptions = React.useMemo( + () => convertBackendFilterOptions(backendFilterOptions), + [backendFilterOptions], + ); + + const updateSelectedSourceLabel = React.useCallback((label: string) => { + setSelectedSourceLabel(label); + }, []); + + const updateFilters = React.useCallback((newFilters: McpServerFilterState) => { + setFilters(newFilters); + }, []); + + const updateSearchTerm = React.useCallback((term: string) => { + setSearchTerm(term); + }, []); + + const resetFilters = React.useCallback(() => { + setFilters(EMPTY_FILTER_STATE); + setSearchTerm(''); + }, []); + + const contextValue = React.useMemo( + () => ({ + mcpServersLoaded, + mcpServersLoadError, + mcpServers, + mcpSources, + mcpSourcesLoaded, + mcpSourcesLoadError, + selectedSourceLabel, + updateSelectedSourceLabel, + filters, + searchTerm, + updateFilters, + updateSearchTerm, + resetFilters, + filterOptions, + filterOptionsLoaded, + filterOptionsLoadError, + apiState, + refreshAPIState, + refreshMcpServers, + refreshMcpSources, + }), + [ + mcpServersLoaded, + mcpServersLoadError, + mcpServers, + mcpSources, + mcpSourcesLoaded, + mcpSourcesLoadError, + selectedSourceLabel, + updateSelectedSourceLabel, + filters, + searchTerm, + updateFilters, + updateSearchTerm, + resetFilters, + filterOptions, + filterOptionsLoaded, + filterOptionsLoadError, + apiState, + refreshAPIState, + refreshMcpServers, + refreshMcpSources, + ], + ); + + return {children}; +}; + +export const useMcpCatalog = (): McpCatalogContextType => React.useContext(McpCatalogContext); diff --git a/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpCatalogAPIState.tsx b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpCatalogAPIState.tsx new file mode 100644 index 0000000000..85d30a4662 --- /dev/null +++ b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpCatalogAPIState.tsx @@ -0,0 +1,30 @@ +import { APIState, useAPIState } from 'mod-arch-core'; +import React from 'react'; +import { + getMcpFilterOptions, + getMcpServer, + getMcpServers, + getMcpSources, +} from '~/app/api/mcpCatalog/service'; +import { McpCatalogAPIs } from '~/app/pages/mcpCatalog/types'; + +export type McpCatalogAPIState = APIState; + +const useMcpCatalogAPIState = ( + hostPath: string | null, + queryParameters?: Record, +): [apiState: McpCatalogAPIState, refreshAPIState: () => void] => { + const createAPI = React.useCallback( + (path: string) => ({ + getMcpServers: getMcpServers(path, queryParameters), + getMcpServer: getMcpServer(path, queryParameters), + getMcpSources: getMcpSources(path, queryParameters), + getMcpFilterOptions: getMcpFilterOptions(path, queryParameters), + }), + [queryParameters], + ); + + return useAPIState(hostPath, createAPI); +}; + +export default useMcpCatalogAPIState; diff --git a/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpFilterOptions.ts b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpFilterOptions.ts new file mode 100644 index 0000000000..1021a7aa21 --- /dev/null +++ b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpFilterOptions.ts @@ -0,0 +1,26 @@ +import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-core'; +import React from 'react'; +import { McpFilterOptionsList } from '~/app/pages/mcpCatalog/types'; +import { McpCatalogAPIState } from './useMcpCatalogAPIState'; + +const EMPTY_FILTER_OPTIONS: McpFilterOptionsList = { filters: {} }; + +/** + * Hook to fetch filter options for MCP servers. + * Returns available values for each filterable field from the backend. + */ +export const useMcpFilterOptions = ( + apiState: McpCatalogAPIState, +): FetchState => { + const call = React.useCallback>( + (opts) => { + if (!apiState.apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + return apiState.api.getMcpFilterOptions(opts); + }, + [apiState], + ); + + return useFetchState(call, EMPTY_FILTER_OPTIONS, { initialPromisePurity: true }); +}; diff --git a/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServerById.ts b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServerById.ts new file mode 100644 index 0000000000..8451580a78 --- /dev/null +++ b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServerById.ts @@ -0,0 +1,30 @@ +import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-core'; +import React from 'react'; +import { McpServer } from '~/app/pages/mcpCatalog/types'; +import { McpCatalogAPIState } from './useMcpCatalogAPIState'; + +const DEFAULT_MCP_SERVER: McpServer = { + id: '', + name: '', + description: '', +}; + +export const useMcpServerById = ( + apiState: McpCatalogAPIState, + serverId: string | undefined, +): FetchState => { + const call = React.useCallback>( + (opts) => { + if (!apiState.apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + if (!serverId) { + return Promise.reject(new Error('Server ID is required')); + } + return apiState.api.getMcpServer(opts, serverId); + }, + [apiState, serverId], + ); + + return useFetchState(call, DEFAULT_MCP_SERVER, { initialPromisePurity: true }); +}; diff --git a/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServers.ts b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServers.ts new file mode 100644 index 0000000000..5b95b120fd --- /dev/null +++ b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpServers.ts @@ -0,0 +1,41 @@ +import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-core'; +import React from 'react'; +import { McpServerList } from '~/app/pages/mcpCatalog/types'; +import { McpCatalogAPIState } from './useMcpCatalogAPIState'; + +// Default page size for fetching MCP servers - set high to get all servers in one request +const DEFAULT_PAGE_SIZE = 100; + +export type UseMcpServersOptions = { + filterQuery?: string; + searchTerm?: string; +}; + +export const useMcpServers = ( + apiState: McpCatalogAPIState, + options?: UseMcpServersOptions, +): FetchState => { + const { filterQuery, searchTerm } = options ?? {}; + + const call = React.useCallback>( + (opts) => { + if (!apiState.apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + return apiState.api.getMcpServers( + opts, + undefined, + DEFAULT_PAGE_SIZE, + filterQuery, + searchTerm, + ); + }, + [apiState, filterQuery, searchTerm], + ); + + return useFetchState( + call, + { items: [], size: 0, pageSize: 0, nextPageToken: '' }, + { initialPromisePurity: true }, + ); +}; diff --git a/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpSources.ts b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpSources.ts new file mode 100644 index 0000000000..b36e0e9a9a --- /dev/null +++ b/clients/ui/frontend/src/app/hooks/mcpCatalog/useMcpSources.ts @@ -0,0 +1,22 @@ +import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-core'; +import React from 'react'; +import { McpCatalogSourceList } from '~/app/pages/mcpCatalog/types'; +import { McpCatalogAPIState } from './useMcpCatalogAPIState'; + +export const useMcpSources = (apiState: McpCatalogAPIState): FetchState => { + const call = React.useCallback>( + (opts) => { + if (!apiState.apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + return apiState.api.getMcpSources(opts); + }, + [apiState], + ); + + return useFetchState( + call, + { items: [], size: 0, pageSize: 0, nextPageToken: '' }, + { initialPromisePurity: true }, + ); +}; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/EmptyMcpCatalogState.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/EmptyMcpCatalogState.tsx new file mode 100644 index 0000000000..49782cde5a --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/EmptyMcpCatalogState.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateVariant, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; + +type EmptyMcpCatalogStateType = { + testid?: string; + title: string; + description: React.ReactNode; + headerIcon?: React.ComponentType; + children?: React.ReactNode; + customAction?: React.ReactNode; +}; + +const EmptyMcpCatalogState: React.FC = ({ + testid, + title, + description, + headerIcon, + children, + customAction, +}) => ( + + {description} + {children} + + {customAction && {customAction}} + + +); + +export default EmptyMcpCatalogState; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogCoreLoader.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogCoreLoader.tsx new file mode 100644 index 0000000000..3f6a188689 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogCoreLoader.tsx @@ -0,0 +1,101 @@ +import { Alert, Bullseye } from '@patternfly/react-core'; +import { useThemeContext } from 'mod-arch-kubeflow'; +import { + ApplicationsPage, + KubeflowDocs, + ProjectObjectType, + TitleWithIcon, + typedEmptyImage, + WhosMyAdministrator, +} from 'mod-arch-shared'; +import * as React from 'react'; +import { Outlet } from 'react-router-dom'; +import { + McpCatalogContext, + McpCatalogContextProvider, +} from '~/app/context/mcpCatalog/McpCatalogContext'; +import EmptyMcpCatalogState from './EmptyMcpCatalogState'; + +/** + * McpCatalogCoreContent handles loading, error, and empty states for MCP Catalog sources. + * Mirrors the pattern used in ModelCatalogCoreLoader for consistency. + */ +const McpCatalogCoreContent: React.FC = () => { + const { mcpSources, mcpSourcesLoaded, mcpSourcesLoadError } = React.useContext(McpCatalogContext); + + const { isMUITheme } = useThemeContext(); + + if (mcpSourcesLoadError) { + return ( + } + description="Discover MCP servers that are available for your organization to deploy and use." + headerContent={null} + empty + emptyStatePage={ + + + {mcpSourcesLoadError.message} + + + } + loaded + /> + ); + } + + if (!mcpSourcesLoaded) { + return ( + } + description="Discover MCP servers that are available for your organization to deploy and use." + headerContent={null} + empty + emptyStatePage={Loading MCP catalog sources...} + loaded={false} + /> + ); + } + + if (mcpSources?.items?.length === 0) { + return ( + } + description="Discover MCP servers that are available for your organization to deploy and use." + empty + emptyStatePage={ + ( + + )} + customAction={isMUITheme ? : } + /> + } + headerContent={null} + loaded + provideChildrenPadding + /> + ); + } + + return ; +}; + +/** + * McpCatalogCoreLoader wraps the MCP Catalog routes with the context provider + * to provide API state and MCP server data to all child components. + */ +const McpCatalogCoreLoader: React.FC = () => ( + + + +); + +export default McpCatalogCoreLoader; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogRoutes.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogRoutes.tsx new file mode 100644 index 0000000000..1c30ce65fe --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/McpCatalogRoutes.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import McpCatalogCoreLoader from './McpCatalogCoreLoader'; +import McpCatalog from './screens/McpCatalog'; +import McpServerDetailsPage from './screens/McpServerDetailsPage'; + +const McpCatalogRoutes: React.FC = () => ( + + }> + } /> + } /> + } /> + + +); + +export default McpCatalogRoutes; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCard.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCard.tsx new file mode 100644 index 0000000000..5699138c93 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCard.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + CardTitle, + Flex, + FlexItem, + Label, + Skeleton, + Truncate, +} from '@patternfly/react-core'; +import McpCatalogCardBody from '~/app/pages/mcpCatalog/components/McpCatalogCardBody'; +import McpCatalogLabels from '~/app/pages/mcpCatalog/components/McpCatalogLabels'; +import { getMcpServerDetailsRoute } from '~/app/routes/mcpCatalog/mcpServerDetails'; +import { McpServer, McpDeploymentMode } from '~/app/pages/mcpCatalog/types'; + +type McpCatalogCardProps = { + server: McpServer; +}; + +const McpCatalogCard: React.FC = ({ server }) => { + const isRemote = server.deploymentMode === McpDeploymentMode.REMOTE; + + return ( + + + + + {server.logo ? ( + + ) : ( + + )} + {isRemote && ( + + Remote + + )} + + + + + + + + + + + + + + ); +}; + +export default McpCatalogCard; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCardBody.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCardBody.tsx new file mode 100644 index 0000000000..215596c924 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCardBody.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import McpSecurityIndicators from '~/app/pages/mcpCatalog/components/McpSecurityIndicators'; +import { McpServer } from '~/app/pages/mcpCatalog/types'; + +type McpCatalogCardBodyProps = { + server: McpServer; +}; + +const McpCatalogCardBody: React.FC = ({ server }) => ( + + + + {server.description} + + + {server.securityIndicators && ( + + + + )} + +); + +export default McpCatalogCardBody; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCategorySection.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCategorySection.tsx new file mode 100644 index 0000000000..d44e1538f4 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCategorySection.tsx @@ -0,0 +1,151 @@ +import { + Alert, + Button, + Flex, + FlexItem, + Grid, + GridItem, + Skeleton, + StackItem, + Title, +} from '@patternfly/react-core'; +import React from 'react'; +import { ArrowRightIcon, SearchIcon } from '@patternfly/react-icons'; +import EmptyMcpCatalogState from '~/app/pages/mcpCatalog/EmptyMcpCatalogState'; +import { McpCatalogSourceList, McpServer, McpSourceLabel } from '~/app/pages/mcpCatalog/types'; +import McpCatalogCard from './McpCatalogCard'; + +type McpCatalogCategorySectionProps = { + label: string; + pageSize: number; + servers: McpServer[]; + sources: McpCatalogSourceList | null; + loaded: boolean; + loadError?: Error; + onShowMore: (label: string) => void; + displayName?: string; +}; + +/** + * Category section for the All Servers view. + * Groups servers by source label. Server-side filtering is already applied via context. + * Only source label filtering is done client-side. + */ +const McpCatalogCategorySection: React.FC = ({ + label, + pageSize, + servers, + sources, + loaded, + loadError, + onShowMore, + displayName, +}) => { + // Filter servers by source label (client-side - source labels are UI grouping only) + const filteredServers = React.useMemo(() => { + if (!sources?.items) { + return []; + } + + // Find sources matching this label + // For McpSourceLabel.other ("null"), find sources with empty labels (catch-all category) + const isOtherCategory = label === McpSourceLabel.other; + const matchingSources = sources.items.filter((source) => { + if (isOtherCategory) { + // Match sources with no labels or only empty/whitespace labels + return source.labels.length === 0 || source.labels.every((l) => !l.trim()); + } + return source.labels.includes(label); + }); + const matchingSourceIds = matchingSources.map((s) => s.id); + + // Filter servers by source_id + return servers.filter((server) => { + if (!server.source_id) { + return false; + } + return matchingSourceIds.includes(server.source_id); + }); + }, [servers, sources, label]); + + const itemsToDisplay = filteredServers.slice(0, pageSize); + + return ( + + + + + {`${displayName ?? label} servers`} + + + + {filteredServers.length >= pageSize && ( + + } + iconPosition="end" + data-testid={`show-more-button ${label.toLowerCase().replace(/\s+/g, '-')}`} + onClick={() => onShowMore(label)} + > + Show all {displayName ?? label} servers + + + )} + + + {loadError ? ( + + {loadError.message} + + ) : !loaded ? ( + + {Array.from({ length: 4 }).map((_, index) => ( + + + + ))} + + ) : filteredServers.length === 0 ? ( + + ) : ( + + {itemsToDisplay.map((server) => ( + + + + ))} + + )} + + ); +}; + +export default McpCatalogCategorySection; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogFilters.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogFilters.tsx new file mode 100644 index 0000000000..4c3aafa95d --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogFilters.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { Divider, Stack, StackItem } from '@patternfly/react-core'; +import McpCatalogStringFilter from '~/app/pages/mcpCatalog/components/McpCatalogStringFilter'; + +type McpCatalogFiltersProps = { + allProviders: string[]; + allLicenses: string[]; + allTags: string[]; + allTransports: string[]; + allDeploymentModes: string[]; + selectedProviders: string[]; + selectedLicenses: string[]; + selectedTags: string[]; + selectedTransports: string[]; + selectedDeploymentModes: string[]; + onProviderChange: (provider: string, checked: boolean) => void; + onLicenseChange: (license: string, checked: boolean) => void; + onTagChange: (tag: string, checked: boolean) => void; + onTransportChange: (transport: string, checked: boolean) => void; + onDeploymentModeChange: (mode: string, checked: boolean) => void; +}; + +const McpCatalogFilters: React.FC = ({ + allProviders, + allLicenses, + allTags, + allTransports, + allDeploymentModes, + selectedProviders, + selectedLicenses, + selectedTags, + selectedTransports, + selectedDeploymentModes, + onProviderChange, + onLicenseChange, + onTagChange, + onTransportChange, + onDeploymentModeChange, +}) => ( + + {allDeploymentModes.length > 0 && ( + <> + + + + + > + )} + {allTransports.length > 0 && ( + <> + + + + + > + )} + {allProviders.length > 0 && ( + <> + + + + + > + )} + {allTags.length > 0 && ( + <> + + + + + > + )} + {allLicenses.length > 0 && ( + + + + )} + +); + +export default McpCatalogFilters; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogLabels.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogLabels.tsx new file mode 100644 index 0000000000..3717bb6271 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogLabels.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Label, LabelGroup } from '@patternfly/react-core'; + +type McpCatalogLabelsProps = { + tags?: string[]; + provider?: string; + numLabels?: number; +}; + +const McpCatalogLabels: React.FC = ({ + tags = [], + provider, + numLabels = 3, +}) => ( + + {tags.map((tag) => ( + + {tag} + + ))} + {provider && ( + + {provider} + + )} + +); + +export default McpCatalogLabels; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogSourceLabelBlocks.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogSourceLabelBlocks.tsx new file mode 100644 index 0000000000..ee4e8cf09e --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogSourceLabelBlocks.tsx @@ -0,0 +1,81 @@ +import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; +import React from 'react'; +import { useMcpCatalog } from '~/app/context/mcpCatalog/McpCatalogContext'; +import { McpCategoryName, McpSourceLabel } from '~/app/pages/mcpCatalog/types'; +import { + getUniqueMcpSourceLabels, + filterEnabledMcpSources, + hasMcpSourcesWithoutLabels, +} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; + +type SourceLabelBlock = { + id: string; + label: string; + displayName: string; +}; + +const McpCatalogSourceLabelBlocks: React.FC = () => { + const { mcpSources, updateSelectedSourceLabel, selectedSourceLabel } = useMcpCatalog(); + + const blocks: SourceLabelBlock[] = React.useMemo(() => { + if (!mcpSources) { + return []; + } + + const enabledSources = filterEnabledMcpSources(mcpSources); + const uniqueLabels = getUniqueMcpSourceLabels(enabledSources); + const hasNoLabels = hasMcpSourcesWithoutLabels(mcpSources); + + const allBlock: SourceLabelBlock = { + id: 'all', + label: McpCategoryName.allServers, + displayName: McpCategoryName.allServers, + }; + + const labelBlocks: SourceLabelBlock[] = uniqueLabels.map((label) => ({ + id: `label-${label.toLowerCase().replace(/\s+/g, '-')}`, + label, + displayName: `${label} servers`, + })); + + const blocksToReturn: SourceLabelBlock[] = [allBlock, ...labelBlocks]; + + if (hasNoLabels) { + const noLabelsBlock: SourceLabelBlock = { + id: 'no-labels', + label: McpSourceLabel.other, + displayName: `${McpCategoryName.communityAndCustomServers} servers`, + }; + blocksToReturn.push(noLabelsBlock); + } + + return blocksToReturn; + }, [mcpSources]); + + if (!mcpSources) { + return null; + } + + const handleToggleClick = (label: string) => { + updateSelectedSourceLabel(label); + }; + + return ( + + {blocks.map((block) => ( + { + handleToggleClick(block.label); + }} + /> + ))} + + ); +}; + +export default McpCatalogSourceLabelBlocks; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogStringFilter.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogStringFilter.tsx new file mode 100644 index 0000000000..f627cfdbe7 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogStringFilter.tsx @@ -0,0 +1,91 @@ +import { Button, Checkbox, Content, ContentVariants, SearchInput } from '@patternfly/react-core'; +import * as React from 'react'; + +const MAX_VISIBLE_FILTERS = 5; + +type McpCatalogStringFilterProps = { + title: string; + values: string[]; + selectedValues: string[]; + onSelectionChange: (value: string, checked: boolean) => void; +}; + +const McpCatalogStringFilter: React.FC = ({ + title, + values, + selectedValues, + onSelectionChange, +}) => { + const [showMore, setShowMore] = React.useState(false); + const [searchValue, setSearchValue] = React.useState(''); + + const valuesMatchingSearch = React.useMemo( + () => + values.filter((value) => { + const lowerValue = value.toLowerCase(); + const isSelected = selectedValues.includes(value); + return lowerValue.includes(searchValue.trim().toLowerCase()) || isSelected; + }), + [values, selectedValues, searchValue], + ); + + const onSearchChange = (newValue: string) => { + setSearchValue(newValue); + }; + + const visibleValues = showMore + ? valuesMatchingSearch + : valuesMatchingSearch.slice(0, MAX_VISIBLE_FILTERS); + + if (values.length === 0) { + return null; + } + + return ( + + {title} + {values.length > MAX_VISIBLE_FILTERS && ( + onSearchChange(newValue)} + /> + )} + {visibleValues.length === 0 && ( + No results found + )} + {visibleValues.map((checkbox) => ( + onSelectionChange(checkbox, checked)} + /> + ))} + {!showMore && valuesMatchingSearch.length > MAX_VISIBLE_FILTERS && ( + setShowMore(true)} + data-testid={`${title}-filter-show-more`} + > + Show more + + )} + {showMore && valuesMatchingSearch.length > MAX_VISIBLE_FILTERS && ( + setShowMore(false)} + data-testid={`${title}-filter-show-less`} + > + Show less + + )} + + ); +}; + +export default McpCatalogStringFilter; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpSecurityIndicators.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpSecurityIndicators.tsx new file mode 100644 index 0000000000..090aea1cb0 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpSecurityIndicators.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { Flex, FlexItem, Icon, Tooltip } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + ShieldAltIcon, + LockIcon, + CodeIcon, + EyeIcon, +} from '@patternfly/react-icons'; +import { McpSecurityIndicator } from '~/app/pages/mcpCatalog/types'; + +type McpSecurityIndicatorsProps = { + indicators: McpSecurityIndicator; +}; + +type IndicatorItemProps = { + isEnabled: boolean; + enabledLabel: string; + disabledLabel: string; + icon: React.ReactNode; +}; + +const IndicatorItem: React.FC = ({ + isEnabled, + enabledLabel, + disabledLabel, + icon, +}) => ( + + + + + {isEnabled ? : } + + {icon} + + {isEnabled ? enabledLabel : disabledLabel} + + + + +); + +const McpSecurityIndicators: React.FC = ({ indicators }) => ( + + } + /> + } + /> + {indicators.sast && ( + } + /> + )} + {indicators.readOnlyTools && ( + } + /> + )} + +); + +export default McpSecurityIndicators; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpToolsList.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpToolsList.tsx new file mode 100644 index 0000000000..1deffa7f21 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/components/McpToolsList.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { + Card, + CardBody, + CardExpandableContent, + CardHeader, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + FlexItem, + Label, + Pagination, + PaginationVariant, + Title, + Tooltip, +} from '@patternfly/react-core'; +import { BanIcon } from '@patternfly/react-icons'; +import { McpTool } from '~/app/pages/mcpCatalog/types'; + +type McpToolsListProps = { + tools: McpTool[]; +}; + +const TOOLS_PER_PAGE = 5; + +const McpToolsList: React.FC = ({ tools }) => { + const [expandedTools, setExpandedTools] = React.useState>(new Set()); + const [page, setPage] = React.useState(1); + + const totalTools = tools.length; + const startIndex = (page - 1) * TOOLS_PER_PAGE; + const endIndex = Math.min(startIndex + TOOLS_PER_PAGE, totalTools); + const paginatedTools = tools.slice(startIndex, endIndex); + + const toggleExpanded = (toolName: string) => { + setExpandedTools((prev) => { + const next = new Set(prev); + if (next.has(toolName)) { + next.delete(toolName); + } else { + next.add(toolName); + } + return next; + }); + }; + + return ( + + + + + + Tools + + + + setPage(newPage)} + variant={PaginationVariant.top} + isCompact + /> + + + + + {paginatedTools.map((tool) => { + const isExpanded = expandedTools.has(tool.name); + const isRevoked = tool.revoked === true; + return ( + + toggleExpanded(tool.name)} + isToggleRightAligned + toggleButtonProps={{ + 'aria-label': `Toggle ${tool.name} details`, + 'aria-expanded': isExpanded, + }} + > + + + {tool.name} + + {isRevoked && ( + + + } isCompact> + Revoked + + + + )} + + + + + + + {tool.description} + + {tool.parameters && tool.parameters.length > 0 && ( + + Input Parameters: + + {tool.parameters.map((param) => ( + + + + + {param.name} + + + + {param.type} + + + + + {param.required ? 'required' : 'optional'} + + + + {param.description} + + + ))} + + + )} + + + + + ); + })} + + + ); +}; + +export default McpToolsList; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx new file mode 100644 index 0000000000..f9ba792564 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx @@ -0,0 +1,285 @@ +import * as React from 'react'; +import { + Button, + Flex, + PageSection, + Sidebar, + SidebarContent, + SidebarPanel, + Stack, + StackItem, + Toolbar, + ToolbarContent, + ToolbarItem, + ToolbarToggleGroup, + ToolbarGroup, + Spinner, + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateVariant, +} from '@patternfly/react-core'; +import { FilterIcon, ExclamationCircleIcon, ArrowRightIcon } from '@patternfly/react-icons'; +import { ApplicationsPage, ProjectObjectType, TitleWithIcon } from 'mod-arch-shared'; +import { useThemeContext } from 'mod-arch-kubeflow'; +import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount'; +import McpCatalogFilters from '~/app/pages/mcpCatalog/components/McpCatalogFilters'; +import McpCatalogSourceLabelBlocks from '~/app/pages/mcpCatalog/components/McpCatalogSourceLabelBlocks'; +import { useMcpCatalog } from '~/app/context/mcpCatalog/McpCatalogContext'; +import { McpCategoryName } from '~/app/pages/mcpCatalog/types'; +import { hasMcpFiltersActive } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import ThemeAwareSearchInput from '~/app/pages/modelRegistry/screens/components/ThemeAwareSearchInput'; +import McpCatalogAllServersView from './McpCatalogAllServersView'; +import McpCatalogGalleryView from './McpCatalogGalleryView'; + +const McpCatalog: React.FC = () => { + const { + mcpServersLoaded, + mcpServersLoadError, + selectedSourceLabel, + filters, + searchTerm, + updateFilters, + updateSearchTerm, + resetFilters, + filterOptions, + filterOptionsLoaded, + } = useMcpCatalog(); + + const [inputValue, setInputValue] = React.useState(''); + const { isMUITheme } = useThemeContext(); + + const isAllServersView = selectedSourceLabel === McpCategoryName.allServers && !searchTerm; + + const handleSearch = React.useCallback( + (value: string) => { + updateSearchTerm(value); + }, + [updateSearchTerm], + ); + + const handleClearSearch = React.useCallback(() => { + updateSearchTerm(''); + setInputValue(''); + }, [updateSearchTerm]); + + const handleSearchInputChange = React.useCallback((value: string) => { + setInputValue(value); + }, []); + + const handleSearchInputSearch = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + handleSearch(value.trim()); + }, + [handleSearch], + ); + + const handleSearchButtonClick = React.useCallback(() => { + if (inputValue.trim() !== searchTerm) { + handleSearch(inputValue.trim()); + } + }, [inputValue, searchTerm, handleSearch]); + + // Handle filter changes - update context filters which triggers server-side refetch + const handleProviderChange = React.useCallback( + (provider: string, checked: boolean) => { + const newProviders = checked + ? [...filters.selectedProviders, provider] + : filters.selectedProviders.filter((p) => p !== provider); + updateFilters({ ...filters, selectedProviders: newProviders }); + }, + [filters, updateFilters], + ); + + const handleLicenseChange = React.useCallback( + (license: string, checked: boolean) => { + const newLicenses = checked + ? [...filters.selectedLicenses, license] + : filters.selectedLicenses.filter((l) => l !== license); + updateFilters({ ...filters, selectedLicenses: newLicenses }); + }, + [filters, updateFilters], + ); + + const handleTagChange = React.useCallback( + (tag: string, checked: boolean) => { + const newTags = checked + ? [...filters.selectedTags, tag] + : filters.selectedTags.filter((t) => t !== tag); + updateFilters({ ...filters, selectedTags: newTags }); + }, + [filters, updateFilters], + ); + + const handleTransportChange = React.useCallback( + (transport: string, checked: boolean) => { + const newTransports = checked + ? [...filters.selectedTransports, transport] + : filters.selectedTransports.filter((t) => t !== transport); + updateFilters({ ...filters, selectedTransports: newTransports }); + }, + [filters, updateFilters], + ); + + const handleDeploymentModeChange = React.useCallback( + (mode: string, checked: boolean) => { + const newModes = checked + ? [...filters.selectedDeploymentModes, mode] + : filters.selectedDeploymentModes.filter((m) => m !== mode); + updateFilters({ ...filters, selectedDeploymentModes: newModes }); + }, + [filters, updateFilters], + ); + + // Check if any filters are active + const hasActiveFilters = searchTerm.length > 0 || hasMcpFiltersActive(filters); + + const handleResetAllFilters = React.useCallback(() => { + resetFilters(); + setInputValue(''); + }, [resetFilters]); + + // Show loading state (wait for both servers and filter options to load) + if (!mcpServersLoaded || !filterOptionsLoaded) { + return ( + } + description="Loading MCP servers..." + empty={false} + loaded={false} + provideChildrenPadding + > + + + + + ); + } + + // Show error state + if (mcpServersLoadError) { + return ( + } + description="Error loading MCP servers" + empty={false} + loaded + provideChildrenPadding + > + + + + {mcpServersLoadError.message || + 'An unexpected error occurred while loading MCP servers.'} + + + + + ); + } + + return ( + <> + + } + description="Discover MCP servers that are available for your organization to integrate with AI agents." + empty={false} + loaded + provideChildrenPadding + > + + + + + + + + + + + }> + + + + + + {isMUITheme && ( + } + iconPosition="end" + onClick={handleSearchButtonClick} + /> + )} + + + + + + + + + + + + + {isAllServersView ? : } + + + + + + + > + ); +}; + +export default McpCatalog; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogAllServersView.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogAllServersView.tsx new file mode 100644 index 0000000000..951524b162 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogAllServersView.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Stack } from '@patternfly/react-core'; +import { useMcpCatalog } from '~/app/context/mcpCatalog/McpCatalogContext'; +import { + filterEnabledMcpSources, + getUniqueMcpSourceLabels, + hasMcpSourcesWithoutLabels, +} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import { McpCategoryName, McpSourceLabel } from '~/app/pages/mcpCatalog/types'; +import McpCatalogCategorySection from '~/app/pages/mcpCatalog/components/McpCatalogCategorySection'; + +/** + * All Servers view showing servers grouped by source label categories. + * Uses server-side filtering via context - no props needed for filters. + */ +const McpCatalogAllServersView: React.FC = () => { + const { + mcpSources, + mcpServers, + mcpServersLoaded, + mcpServersLoadError, + updateSelectedSourceLabel, + } = useMcpCatalog(); + + const sourceLabels = React.useMemo(() => { + const enabledSources = filterEnabledMcpSources(mcpSources); + return getUniqueMcpSourceLabels(enabledSources); + }, [mcpSources]); + + const hasSourcesWithoutLabelsValue = React.useMemo( + () => hasMcpSourcesWithoutLabels(mcpSources), + [mcpSources], + ); + + const handleShowMoreCategory = React.useCallback( + (categoryLabel: string) => { + updateSelectedSourceLabel(categoryLabel); + }, + [updateSelectedSourceLabel], + ); + + const servers = mcpServers?.items || []; + + return ( + + {sourceLabels.map((label) => ( + + ))} + {hasSourcesWithoutLabelsValue && ( + + )} + + ); +}; + +export default McpCatalogAllServersView; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx new file mode 100644 index 0000000000..273b5bad7b --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Grid, GridItem } from '@patternfly/react-core'; +import { SearchIcon } from '@patternfly/react-icons'; +import { useMcpCatalog } from '~/app/context/mcpCatalog/McpCatalogContext'; +import { McpCategoryName, McpSourceLabel, McpServer } from '~/app/pages/mcpCatalog/types'; +import { hasMcpFiltersActive } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import McpCatalogCard from '~/app/pages/mcpCatalog/components/McpCatalogCard'; +import EmptyMcpCatalogState from '~/app/pages/mcpCatalog/EmptyMcpCatalogState'; + +/** + * Gallery view for MCP servers. + * Displays servers in a grid layout with filtering applied server-side. + * Client-side filtering is only used for source label filtering (not sent to backend). + */ +const McpCatalogGalleryView: React.FC = () => { + const { mcpServers, mcpSources, selectedSourceLabel, searchTerm, filters } = useMcpCatalog(); + + const servers = React.useMemo(() => mcpServers?.items ?? [], [mcpServers?.items]); + + // Filter servers by selected source label (client-side) + // This is the only client-side filtering needed - source label is UI-only + const filteredServers = React.useMemo((): McpServer[] => { + if (selectedSourceLabel === McpCategoryName.allServers) { + return servers; + } + + if (!mcpSources?.items) { + return []; + } + + // Handle "other" label (sources without labels) + if (selectedSourceLabel === McpSourceLabel.other) { + const sourcesWithoutLabels = mcpSources.items.filter( + (source) => source.labels.length === 0 || source.labels.every((label) => !label.trim()), + ); + const sourceIds = sourcesWithoutLabels.map((s) => s.id); + return servers.filter((server) => server.source_id && sourceIds.includes(server.source_id)); + } + + // Find sources with the selected label + const matchingSources = mcpSources.items.filter((source) => + source.labels.includes(selectedSourceLabel), + ); + const matchingSourceIds = matchingSources.map((s) => s.id); + + return servers.filter( + (server) => server.source_id && matchingSourceIds.includes(server.source_id), + ); + }, [servers, mcpSources, selectedSourceLabel]); + + // Show empty state if no servers match filters + if (filteredServers.length === 0) { + const hasActiveFilters = + searchTerm.length > 0 || + hasMcpFiltersActive(filters) || + selectedSourceLabel !== McpCategoryName.allServers; + return ( + + ); + } + + return ( + + {filteredServers.map((server) => ( + + + + ))} + + ); +}; + +export default McpCatalogGalleryView; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsPage.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsPage.tsx new file mode 100644 index 0000000000..1e1d6186e5 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsPage.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import { + Breadcrumb, + BreadcrumbItem, + Content, + ContentVariants, + Flex, + FlexItem, + Stack, + StackItem, + Spinner, + Bullseye, +} from '@patternfly/react-core'; +import { ApplicationsPage } from 'mod-arch-shared'; +import { mcpCatalogUrl } from '~/app/routes/mcpCatalog/mcpCatalog'; +import { McpServerDetailsParams } from '~/app/routes/mcpCatalog/mcpServerDetails'; +import { useMcpServerById } from '~/app/hooks/mcpCatalog/useMcpServerById'; +import { useMcpCatalog } from '~/app/context/mcpCatalog/McpCatalogContext'; +import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount'; +import McpServerDetailsView from './McpServerDetailsView'; + +const MCP_SERVER_ICON_URL = + 'https://catalog.redhat.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Funvalidated-model-logo.4b98e427.svg&w=96&q=75'; + +const McpServerDetailsPage: React.FC = () => { + const { serverId } = useParams(); + const decodedServerId = serverId ? decodeURIComponent(serverId) : ''; + + const { apiState } = useMcpCatalog(); + const [server, loaded, loadError] = useMcpServerById(apiState, decodedServerId); + + // Check if server was found (id would be empty if not found/default) + const serverNotFound = loaded && !loadError && !server.id; + + // Show loading state + if (!loaded) { + return ( + + + MCP catalog + + Loading... + + } + title="Loading..." + empty={false} + loaded={false} + provideChildrenPadding + > + + + + + ); + } + + return ( + <> + + + + MCP catalog + + {server.name || 'Details'} + + } + title={ + server.id ? ( + + + + + + {server.name} + + + + + Provided by {server.provider || 'Unknown'} + + + + + ) : null + } + empty={serverNotFound} + emptyStatePage={ + serverNotFound ? ( + + Server not found. Return to MCP catalog + + ) : undefined + } + loadError={loadError} + loaded={loaded} + errorMessage="Unable to load MCP server" + provideChildrenPadding + > + {server.id && } + + > + ); +}; + +export default McpServerDetailsPage; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsView.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsView.tsx new file mode 100644 index 0000000000..23c7b1f71f --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsView.tsx @@ -0,0 +1,259 @@ +import * as React from 'react'; +import { + Button, + Card, + CardBody, + CardHeader, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Icon, + PageSection, + Sidebar, + SidebarContent, + SidebarPanel, + Stack, + StackItem, + Title, +} from '@patternfly/react-core'; +import { GithubIcon, OutlinedClockIcon } from '@patternfly/react-icons'; +import { InlineTruncatedClipboardCopy } from 'mod-arch-shared'; +import text from '@patternfly/react-styles/css/utilities/Text/text'; +import { McpServer } from '~/app/pages/mcpCatalog/types'; +import { + formatDeploymentMode, + formatTransports, + isRemoteMcpServer, +} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import McpToolsList from '~/app/pages/mcpCatalog/components/McpToolsList'; +import McpCatalogLabels from '~/app/pages/mcpCatalog/components/McpCatalogLabels'; +import ExternalLink from '~/app/shared/components/ExternalLink'; +import MarkdownComponent from '~/app/shared/markdown/MarkdownComponent'; + +type McpServerDetailsViewProps = { + server: McpServer; +}; + +const McpServerDetailsView: React.FC = ({ server }) => { + const isRemote = isRemoteMcpServer(server.deploymentMode); + + return ( + + + + + {/* Description Section */} + + + + + Description + + + + + + {server.description || 'No description'} + + + + + + + {/* MCP Server Card Section */} + + + + + MCP server card + + + + {!server.readme && No MCP server card} + {server.readme && ( + + )} + + + + + + + {/* Details Panel */} + + + + + + + MCP Server Details + + + + + {/* Tags - matching Model's Labels pattern */} + {server.tags && server.tags.length > 0 && ( + + Tags + + + + + )} + {/* License with link - matching Model's License pattern */} + {server.license_link && ( + + License + + + )} + + Provider + + {server.provider || 'N/A'} + + + {!isRemote && server.version && ( + + Version + {server.version} + + )} + + Transport(s) + + {formatTransports(server.transports)} + + + + Deployment Mode + + {formatDeploymentMode(server.deploymentMode)} + + + {isRemote && server.endpoints && ( + <> + {server.endpoints.http && ( + + HTTP Endpoint + + + + + )} + {server.endpoints.sse && ( + + SSE Endpoint + + + + + )} + > + )} + {!isRemote && ( + <> + + MCP server location + + {server.artifacts && + server.artifacts.length > 0 && + server.artifacts[0].uri ? ( + + ) : ( + 'N/A' + )} + + + + Source Code + + {server.sourceCode ? ( + } + > + {server.sourceCode} + + ) : ( + 'N/A' + )} + + + + Last Modified + + + + + {server.lastUpdated + ? new Date(server.lastUpdated).toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }) + : 'N/A'} + + + + Published + + + + + {server.publishedDate + ? new Date(server.publishedDate).toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }) + : 'N/A'} + + + > + )} + + + + + + {/* Tools Section */} + {server.tools && server.tools.length > 0 && ( + + + + )} + + + + + ); +}; + +export default McpServerDetailsView; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/types.ts b/clients/ui/frontend/src/app/pages/mcpCatalog/types.ts new file mode 100644 index 0000000000..d81354aa9c --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/types.ts @@ -0,0 +1,246 @@ +/** + * MCP (Model Context Protocol) Catalog Types + * + * These types define the structure of MCP servers displayed in the catalog. + */ + +import { APIOptions } from 'mod-arch-core'; + +export enum McpToolAccessType { + READ_ONLY = 'read_only', + READ_WRITE = 'read_write', + EXECUTE = 'execute', +} + +export type McpSecurityIndicator = { + verifiedSource: boolean; + secureEndpoint: boolean; + sast: boolean; + readOnlyTools: boolean; +}; + +export type McpToolParameter = { + name: string; + type: string; + description: string; + required: boolean; +}; + +/** + * MetadataValue types following Model Registry patterns. + * Tags are stored as MetadataStringValue entries with empty string_value (label pattern). + * Security indicators are stored as MetadataBoolValue entries. + */ +export type MetadataStringValue = { + string_value: string; + metadataType: 'MetadataStringValue'; +}; + +export type MetadataBoolValue = { + bool_value: boolean; + metadataType: 'MetadataBoolValue'; +}; + +export type MetadataIntValue = { + int_value: string; + metadataType: 'MetadataIntValue'; +}; + +export type MetadataDoubleValue = { + double_value: number; + metadataType: 'MetadataDoubleValue'; +}; + +export type MetadataStructValue = { + struct_value: string; + metadataType: 'MetadataStructValue'; +}; + +export type McpMetadataValue = + | MetadataStringValue + | MetadataBoolValue + | MetadataIntValue + | MetadataDoubleValue + | MetadataStructValue; + +/** + * Custom properties map following Model Registry patterns. + * Keys are property names, values are MetadataValue discriminated union types. + */ +export type McpCustomProperties = Record; + +export type McpTool = { + name: string; + description: string; + accessType: McpToolAccessType; + parameters?: McpToolParameter[]; + /** Whether this tool has been revoked. Revoked tools should not be invoked by AI agents. */ + revoked?: boolean; + /** Human-readable reason why the tool was revoked. */ + revokedReason?: string; + customProperties?: McpCustomProperties; +}; + +export enum McpTransportType { + STDIO = 'stdio', + SSE = 'sse', + HTTP = 'http', +} + +export enum McpDeploymentMode { + LOCAL = 'local', + REMOTE = 'remote', +} + +export type McpEndpoints = { + http?: string; + sse?: string; +}; + +/** + * Artifact for an MCP server (e.g., OCI image for local deployment). + */ +export type McpArtifact = { + uri: string; + createTimeSinceEpoch?: string; + lastUpdateTimeSinceEpoch?: string; +}; + +export type McpServer = { + id: string; + name: string; + description: string; + source_id?: string; + logo?: string; + license?: string; + license_link?: string; + provider?: string; + version?: string; + tags?: string[]; + tools?: McpTool[]; + securityIndicators?: McpSecurityIndicator; + documentationUrl?: string; + repositoryUrl?: string; + sourceCode?: string; + lastUpdated?: string; + publishedDate?: string; + artifacts?: McpArtifact[]; + transports?: McpTransportType[]; + apiKey?: string; + readme?: string; + deploymentMode?: McpDeploymentMode; + endpoints?: McpEndpoints; + /** + * Custom properties following Model Registry patterns. + * Tags are stored as MetadataStringValue entries with empty string_value (label pattern). + * Security indicators are stored as MetadataBoolValue entries. + */ + customProperties?: McpCustomProperties; +}; + +/** + * Asset type for catalog sources + */ +export enum CatalogAssetType { + MODELS = 'models', + MCP_SERVERS = 'mcp_servers', +} + +/** + * MCP Catalog Source - represents a source of MCP servers + */ +export type McpCatalogSource = { + id: string; + name: string; + labels: string[]; + enabled?: boolean; + assetType?: CatalogAssetType; + status?: 'available' | 'error' | 'disabled'; + error?: string; +}; + +export type McpCatalogSourceList = { + items?: McpCatalogSource[]; + size: number; + pageSize: number; + nextPageToken?: string; +}; + +/** + * Category names for MCP server organization + */ +export enum McpCategoryName { + allServers = 'All MCP servers', + communityAndCustomServers = 'Community and custom', +} + +/** + * Special label for sources without labels + */ +export enum McpSourceLabel { + other = 'null', +} + +export type McpServerList = { + items: McpServer[]; + size: number; + pageSize: number; + nextPageToken?: string; +}; + +/** + * Filter key constants for MCP Catalog filters. + * Following Model Catalog pattern with string filter keys. + */ +export enum McpFilterKey { + PROVIDER = 'provider', + LICENSE = 'license', + TAGS = 'tags', + TRANSPORTS = 'transports', + DEPLOYMENT_MODE = 'deploymentMode', +} + +/** + * Filter state for MCP Catalog. + * Each filter key maps to an array of selected values. + */ +export type McpFilterState = { + [McpFilterKey.PROVIDER]: string[]; + [McpFilterKey.LICENSE]: string[]; + [McpFilterKey.TAGS]: string[]; + [McpFilterKey.TRANSPORTS]: string[]; + [McpFilterKey.DEPLOYMENT_MODE]: string[]; +}; + +/** + * Filter option structure matching backend response. + * Each filter option has a type and available values. + */ +export type McpFilterOption = { + type: string; + values?: unknown[]; +}; + +/** + * Filter options list response from the backend. + * Keys are filter field names, values describe available options. + */ +export type McpFilterOptionsList = { + filters?: Record; +}; + +/** + * API interface for MCP Catalog operations + */ +export type McpCatalogAPIs = { + getMcpServers: ( + opts: APIOptions, + sourceLabel?: string, + pageSize?: number, + filterQuery?: string, + searchTerm?: string, + ) => Promise; + getMcpServer: (opts: APIOptions, serverId: string) => Promise; + getMcpSources: (opts: APIOptions) => Promise; + getMcpFilterOptions: (opts: APIOptions) => Promise; +}; diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/utils/mcpCatalogUtils.ts b/clients/ui/frontend/src/app/pages/mcpCatalog/utils/mcpCatalogUtils.ts new file mode 100644 index 0000000000..936c49bd10 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/utils/mcpCatalogUtils.ts @@ -0,0 +1,366 @@ +import { + McpCatalogSource, + McpCatalogSourceList, + McpCustomProperties, + McpDeploymentMode, + McpMetadataValue, + McpSecurityIndicator, + McpServer, + MetadataBoolValue, +} from '~/app/pages/mcpCatalog/types'; + +/** + * Filter state for MCP server sidebar filters + */ +export type McpServerFilterState = { + selectedProviders: string[]; + selectedLicenses: string[]; + selectedTags: string[]; + selectedTransports: string[]; + selectedDeploymentModes: string[]; +}; + +// Helper to wrap value in quotes for filterQuery +const wrapInQuotes = (v: string): string => `'${v}'`; + +// Helper to create equality filter (matching Model Catalog pattern) +const eqFilter = (k: string, v: string): string => `${k}=${wrapInQuotes(v)}`; + +// Helper to create IN filter (matching Model Catalog pattern) +const inFilter = (k: string, values: string[]): string => + `${k} IN (${values.map((v) => wrapInQuotes(v)).join(',')})`; + +/** + * Convert MCP server filter state to a filterQuery string for server-side filtering. + * Uses SQL-like syntax matching Model Catalog: field='value', field IN ('a','b') + * + * @param filters - The current filter state + * @returns A filterQuery string or empty string if no filters are active + */ +export const mcpFiltersToFilterQuery = (filters: McpServerFilterState): string => { + const queryParts: string[] = []; + + // Provider filter + if (filters.selectedProviders.length === 1) { + queryParts.push(eqFilter('provider', filters.selectedProviders[0])); + } else if (filters.selectedProviders.length > 1) { + queryParts.push(inFilter('provider', filters.selectedProviders)); + } + + // License filter + if (filters.selectedLicenses.length === 1) { + queryParts.push(eqFilter('license', filters.selectedLicenses[0])); + } else if (filters.selectedLicenses.length > 1) { + queryParts.push(inFilter('license', filters.selectedLicenses)); + } + + // Tags filter (array field - backend handles array matching) + if (filters.selectedTags.length === 1) { + queryParts.push(eqFilter('tags', filters.selectedTags[0])); + } else if (filters.selectedTags.length > 1) { + queryParts.push(inFilter('tags', filters.selectedTags)); + } + + // Transport filter (array field - backend handles array matching) + if (filters.selectedTransports.length === 1) { + queryParts.push(eqFilter('transports', filters.selectedTransports[0])); + } else if (filters.selectedTransports.length > 1) { + queryParts.push(inFilter('transports', filters.selectedTransports)); + } + + // Deployment mode filter + if (filters.selectedDeploymentModes.length === 1) { + queryParts.push(eqFilter('deploymentMode', filters.selectedDeploymentModes[0])); + } else if (filters.selectedDeploymentModes.length > 1) { + queryParts.push(inFilter('deploymentMode', filters.selectedDeploymentModes)); + } + + return queryParts.length === 0 ? '' : queryParts.join(' AND '); +}; + +/** + * Check if any filters are currently active + */ +export const hasMcpFiltersActive = (filters: McpServerFilterState): boolean => + filters.selectedProviders.length > 0 || + filters.selectedLicenses.length > 0 || + filters.selectedTags.length > 0 || + filters.selectedTransports.length > 0 || + filters.selectedDeploymentModes.length > 0; + +/** + * Format a transport type for display + */ +export const formatTransportType = (transport: string | undefined): string => { + if (!transport) { + return 'N/A'; + } + return transport.toUpperCase(); +}; + +/** + * Format transports array for display + */ +export const formatTransports = (transports: string[] | undefined): string => { + if (!transports || transports.length === 0) { + return 'N/A'; + } + return transports.map((t) => t.toUpperCase()).join(', '); +}; + +/** + * Format deployment mode for display + */ +export const formatDeploymentMode = (deploymentMode: McpDeploymentMode | undefined): string => { + switch (deploymentMode) { + case McpDeploymentMode.REMOTE: + return 'Remote'; + case McpDeploymentMode.LOCAL: + return 'Local'; + default: + return 'Local'; + } +}; + +/** + * Check if the server is a remote deployment + */ +export const isRemoteMcpServer = (deploymentMode: McpDeploymentMode | undefined): boolean => + deploymentMode === McpDeploymentMode.REMOTE; + +/** + * Filter catalog sources to only include enabled sources + */ +export const filterEnabledMcpSources = ( + sources: McpCatalogSourceList | null, +): McpCatalogSourceList | null => { + if (!sources) { + return null; + } + + const filteredItems = sources.items?.filter((source) => source.enabled !== false); + + return { + ...sources, + items: filteredItems || [], + size: filteredItems?.length || 0, + }; +}; + +/** + * Get unique source labels from catalog sources + */ +export const getUniqueMcpSourceLabels = (sources: McpCatalogSourceList | null): string[] => { + if (!sources || !sources.items) { + return []; + } + + const allLabels = new Set(); + + sources.items.forEach((source) => { + if (source.enabled !== false && source.labels.length > 0) { + source.labels.forEach((label) => { + if (label.trim()) { + allLabels.add(label.trim()); + } + }); + } + }); + + return Array.from(allLabels); +}; + +/** + * Check if any sources exist without labels + */ +export const hasMcpSourcesWithoutLabels = (sources: McpCatalogSourceList | null): boolean => { + if (!sources || !sources.items) { + return false; + } + + return sources.items.some((source) => { + if (source.enabled !== false) { + return source.labels.length === 0 || source.labels.every((label) => !label.trim()); + } + return false; + }); +}; + +/** + * Get source from source ID + */ +export const getMcpSourceFromSourceId = ( + sourceId: string, + sources: McpCatalogSourceList | null, +): McpCatalogSource | undefined => { + if (!sources || !sourceId || !sources.items) { + return undefined; + } + + return sources.items.find((source) => source.id === sourceId); +}; + +// ============================================================================ +// CustomProperties Utility Functions +// Following Model Registry patterns for label and property handling +// ============================================================================ + +/** + * Type guard to check if a MetadataValue is a string value + */ +const isMetadataStringValue = (value: McpMetadataValue): boolean => + value.metadataType === 'MetadataStringValue'; + +/** + * Type guard to check if a MetadataValue is a bool value + */ +const isMetadataBoolValue = (value: McpMetadataValue): value is MetadataBoolValue => + value.metadataType === 'MetadataBoolValue'; + +/** + * Extract tags from customProperties. + * Tags are stored as MetadataStringValue entries with EMPTY string_value (label pattern). + * This follows the Model Registry pattern where labels are empty string properties. + * + * @param customProperties - The customProperties map from McpServer + * @returns Array of tag names (the keys of entries with empty string_value) + */ +export const getMcpServerTags = (customProperties: McpCustomProperties | undefined): string[] => { + if (!customProperties) { + return []; + } + + return Object.entries(customProperties) + .filter(([, value]) => { + if (!isMetadataStringValue(value)) { + return false; + } + // Tags have empty string_value (label pattern) + return 'string_value' in value && value.string_value === ''; + }) + .map(([key]) => key); +}; + +/** + * Extract security indicators from customProperties. + * Security indicators are stored as MetadataBoolValue entries with specific keys. + * + * @param customProperties - The customProperties map from McpServer + * @returns McpSecurityIndicator object or undefined if no security properties exist + */ +export const getMcpSecurityIndicatorsFromCustomProperties = ( + customProperties: McpCustomProperties | undefined, +): McpSecurityIndicator | undefined => { + if (!customProperties) { + return undefined; + } + + const securityKeys = ['verifiedSource', 'secureEndpoint', 'sast', 'readOnlyTools']; + const hasAnySecurityIndicator = securityKeys.some( + (key) => key in customProperties && isMetadataBoolValue(customProperties[key]), + ); + + if (!hasAnySecurityIndicator) { + return undefined; + } + + const getBoolValue = (key: string): boolean => { + const value = customProperties[key]; + return isMetadataBoolValue(value) ? value.bool_value : false; + }; + + return { + verifiedSource: getBoolValue('verifiedSource'), + secureEndpoint: getBoolValue('secureEndpoint'), + sast: getBoolValue('sast'), + readOnlyTools: getBoolValue('readOnlyTools'), + }; +}; + +/** + * Extract string properties from customProperties (non-empty string_value entries). + * This follows the Model Registry pattern where properties have non-empty string values. + * + * @param customProperties - The customProperties map from McpServer + * @returns Record of property name to string value + */ +export const getMcpServerProperties = ( + customProperties: McpCustomProperties | undefined, +): Record => { + if (!customProperties) { + return {}; + } + + const result: Record = {}; + + Object.entries(customProperties).forEach(([key, value]) => { + if (isMetadataStringValue(value) && 'string_value' in value && value.string_value !== '') { + result[key] = value.string_value; + } + }); + + return result; +}; + +/** + * Get a specific string property value from customProperties. + * + * @param customProperties - The customProperties map from McpServer + * @param key - The property key to retrieve + * @returns The string value or undefined if not found + */ +export const getMcpCustomPropertyString = ( + customProperties: McpCustomProperties | undefined, + key: string, +): string | undefined => { + if (!customProperties || !(key in customProperties)) { + return undefined; + } + + const value = customProperties[key]; + if (isMetadataStringValue(value) && 'string_value' in value) { + return value.string_value; + } + + return undefined; +}; + +/** + * Get tags from an MCP server, preferring customProperties but falling back to tags array. + * This supports backward compatibility where both formats may exist. + * + * @param server - The MCP server + * @returns Array of tag names + */ +export const getServerTags = (server: McpServer): string[] => { + // First try to get from customProperties (Model Registry aligned) + const customPropsTags = getMcpServerTags(server.customProperties); + if (customPropsTags.length > 0) { + return customPropsTags; + } + + // Fall back to first-class tags field (backward compatibility) + return server.tags ?? []; +}; + +/** + * Get security indicators from an MCP server, preferring customProperties but falling back. + * This supports backward compatibility where both formats may exist. + * + * @param server - The MCP server + * @returns McpSecurityIndicator object or undefined + */ +export const getServerSecurityIndicators = ( + server: McpServer, +): McpSecurityIndicator | undefined => { + // First try to get from customProperties (Model Registry aligned) + const customPropsIndicators = getMcpSecurityIndicatorsFromCustomProperties( + server.customProperties, + ); + if (customPropsIndicators) { + return customPropsIndicators; + } + + // Fall back to first-class securityIndicators field (backward compatibility) + return server.securityIndicators; +}; diff --git a/clients/ui/frontend/src/app/routes/mcpCatalog/mcpCatalog.ts b/clients/ui/frontend/src/app/routes/mcpCatalog/mcpCatalog.ts new file mode 100644 index 0000000000..b06f406cd7 --- /dev/null +++ b/clients/ui/frontend/src/app/routes/mcpCatalog/mcpCatalog.ts @@ -0,0 +1,3 @@ +export const MCP_CATALOG_PAGE_TITLE = 'MCP Catalog'; + +export const mcpCatalogUrl = (): string => '/mcp-catalog'; diff --git a/clients/ui/frontend/src/app/routes/mcpCatalog/mcpServerDetails.ts b/clients/ui/frontend/src/app/routes/mcpCatalog/mcpServerDetails.ts new file mode 100644 index 0000000000..7cd5f1a95b --- /dev/null +++ b/clients/ui/frontend/src/app/routes/mcpCatalog/mcpServerDetails.ts @@ -0,0 +1,8 @@ +import { mcpCatalogUrl } from './mcpCatalog'; + +export type McpServerDetailsParams = { + serverId: string; +}; + +export const getMcpServerDetailsRoute = (serverId: string): string => + `${mcpCatalogUrl()}/${encodeURIComponent(serverId)}`; diff --git a/manifests/kustomize/options/catalog/overlays/demo/dev-community-mcp-servers.yaml b/manifests/kustomize/options/catalog/overlays/demo/dev-community-mcp-servers.yaml new file mode 100644 index 0000000000..1e46df110c --- /dev/null +++ b/manifests/kustomize/options/catalog/overlays/demo/dev-community-mcp-servers.yaml @@ -0,0 +1,616 @@ +source: Community and custom +mcp_servers: + - name: prometheus-mcp + provider: Prometheus Community + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Query Prometheus metrics and alerts directly from your agent. + Provides PromQL query execution, alert retrieval, and metric discovery. + readme: |- + # Prometheus MCP Server + + **MCP Server Summary:** + Access Prometheus metrics and alerting data through the MCP protocol. + + ## Features + - Execute PromQL queries + - Query range data + - Retrieve active alerts + + ## Prerequisites + - Prometheus server access + - Network connectivity to Prometheus API + version: "0.9.2" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/prometheus-community/prometheus-mcp + sourceCode: prometheus-community/prometheus-mcp + publishedDate: "2025-01-10" + tools: + - name: query + description: Execute PromQL queries + accessType: read_only + parameters: + - name: query + type: string + description: PromQL query expression + required: true + - name: query_range + description: Execute PromQL range queries + accessType: read_only + parameters: + - name: query + type: string + description: PromQL query expression + required: true + - name: start + type: string + description: Start timestamp + required: true + - name: end + type: string + description: End timestamp + required: true + - name: get_alerts + description: Retrieve active alerts + accessType: read_only + parameters: [] + artifacts: + - uri: oci://ghcr.io/prometheus-community/prometheus-mcp:0.9.2 + createTimeSinceEpoch: "1736510400000" + lastUpdateTimeSinceEpoch: "1736510400000" + customProperties: + # Tags (MetadataStringValue with empty string_value) + metrics: + metadataType: MetadataStringValue + string_value: "" + monitoring: + metadataType: MetadataStringValue + string_value: "" + alerting: + metadataType: MetadataStringValue + string_value: "" + # Security indicators (MetadataBoolValue) + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736510400000" + lastUpdateTimeSinceEpoch: "1736510400000" + + - name: kubernetes-mcp + provider: CNCF + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Interact with Kubernetes clusters through natural language. + List pods, check deployments, view logs, and manage resources. + readme: |- + # Kubernetes MCP Server + + **MCP Server Summary:** + Comprehensive Kubernetes cluster management through MCP. + + ## Features + - Pod management and listing + - Deployment operations + - Log retrieval + - Scaling operations + + ## Prerequisites + - kubectl configured with cluster access + - Appropriate RBAC permissions + version: "1.2.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/cncf/kubernetes-mcp + sourceCode: cncf/kubernetes-mcp + publishedDate: "2025-01-12" + tools: + - name: get_pods + description: List pods in a namespace + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: get_deployments + description: List deployments + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: get_logs + description: Retrieve pod logs + accessType: read_only + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: pod_name + type: string + description: Name of the pod + required: true + - name: scale_deployment + description: Scale a deployment + accessType: read_write + parameters: + - name: namespace + type: string + description: Kubernetes namespace + required: true + - name: deployment_name + type: string + description: Name of the deployment + required: true + - name: replicas + type: number + description: Desired number of replicas + required: true + artifacts: + - uri: oci://ghcr.io/cncf/kubernetes-mcp:1.2.0 + createTimeSinceEpoch: "1736683200000" + lastUpdateTimeSinceEpoch: "1736683200000" + customProperties: + kubernetes: + metadataType: MetadataStringValue + string_value: "" + containers: + metadataType: MetadataStringValue + string_value: "" + orchestration: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736683200000" + lastUpdateTimeSinceEpoch: "1736683200000" + + - name: openshift-mcp + provider: Red Hat + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Red Hat OpenShift integration for container platform management. + Deploy applications, manage routes, and monitor cluster health. + readme: |- + # OpenShift MCP Server + + **MCP Server Summary:** + Red Hat OpenShift container platform integration. + + ## Features + - Project listing + - Route management + - Build operations + + ## Prerequisites + - OpenShift CLI (oc) configured + - Cluster admin or developer access + version: "1.0.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/redhat/openshift-mcp + sourceCode: redhat/openshift-mcp + publishedDate: "2025-01-13" + tools: + - name: get_projects + description: List OpenShift projects + accessType: read_only + parameters: [] + - name: get_routes + description: List routes in a project + accessType: read_only + parameters: + - name: project + type: string + description: Project name + required: true + - name: get_builds + description: List builds and their status + accessType: read_only + parameters: + - name: project + type: string + description: Project name + required: true + - name: start_build + description: Trigger a new build + accessType: read_write + parameters: + - name: project + type: string + description: Project name + required: true + - name: build_config + type: string + description: Build configuration name + required: true + artifacts: + - uri: oci://registry.redhat.io/openshift/openshift-mcp:1.0.0 + createTimeSinceEpoch: "1736769600000" + lastUpdateTimeSinceEpoch: "1736769600000" + customProperties: + openshift: + metadataType: MetadataStringValue + string_value: "" + containers: + metadataType: MetadataStringValue + string_value: "" + kubernetes: + metadataType: MetadataStringValue + string_value: "" + enterprise: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736769600000" + lastUpdateTimeSinceEpoch: "1736769600000" + + - name: elasticsearch-mcp + provider: Elastic + license: elastic-2.0 + license_link: https://www.elastic.co/licensing/elastic-license + description: >- + Query and analyze data in Elasticsearch clusters. + Execute searches, aggregations, and manage indices. + readme: |- + # Elasticsearch MCP Server + + **MCP Server Summary:** + Elasticsearch data query and analysis. + + ## Features + - Search queries + - Aggregations + - Index management + + ## Prerequisites + - Elasticsearch cluster access + - API credentials (if authentication enabled) + version: "0.8.0" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/elastic/elasticsearch-mcp + sourceCode: elastic/elasticsearch-mcp + publishedDate: "2024-12-20" + tools: + - name: search + description: Execute a search query + accessType: read_only + parameters: + - name: index + type: string + description: Index name or pattern + required: true + - name: aggregate + description: Execute an aggregation query + accessType: read_only + parameters: + - name: index + type: string + description: Index name or pattern + required: true + - name: get_indices + description: List available indices + accessType: read_only + parameters: [] + artifacts: + - uri: oci://docker.elastic.co/elasticsearch/elasticsearch-mcp:0.8.0 + createTimeSinceEpoch: "1734696000000" + lastUpdateTimeSinceEpoch: "1734696000000" + customProperties: + search: + metadataType: MetadataStringValue + string_value: "" + analytics: + metadataType: MetadataStringValue + string_value: "" + logging: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: false + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1734696000000" + lastUpdateTimeSinceEpoch: "1734696000000" + + # Remote MCP Servers - hosted externally, accessed via network endpoints + # Note: Remote servers do not have artifacts as they are hosted externally + - name: openai-assistants-mcp + provider: OpenAI + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Access OpenAI Assistants API for advanced AI conversations and tool use. + This is a remotely hosted MCP server provided by OpenAI's cloud infrastructure. + readme: |- + # OpenAI Assistants MCP Server + + **MCP Server Summary:** + Remotely hosted MCP server for OpenAI Assistants integration. + + ## Features + - Create and manage AI assistants + - Execute conversations with tool use + - Retrieve assistant responses + + ## Note + This is a remote MCP server hosted by OpenAI. No local deployment required. + deploymentMode: remote + endpoints: + http: https://api.openai.com/v1/mcp + sse: https://api.openai.com/v1/mcp/stream + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://platform.openai.com/docs/assistants + repositoryUrl: https://github.com/openai/openai-mcp + sourceCode: openai/openai-mcp + publishedDate: "2025-01-20" + tools: + - name: create_assistant + description: Create a new AI assistant + accessType: read_write + parameters: + - name: name + type: string + description: Assistant name + required: true + - name: instructions + type: string + description: System instructions for the assistant + required: true + - name: chat + description: Send a message to an assistant + accessType: read_write + parameters: + - name: assistant_id + type: string + description: ID of the assistant + required: true + - name: message + type: string + description: User message + required: true + - name: list_assistants + description: List available assistants + accessType: read_only + parameters: [] + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + assistants: + metadataType: MetadataStringValue + string_value: "" + cloud: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1737388800000" + lastUpdateTimeSinceEpoch: "1737388800000" + + - name: anthropic-claude-mcp + provider: Anthropic + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Access Anthropic's Claude models through a remotely hosted MCP server. + Enables tool use, multi-turn conversations, and computer use capabilities. + readme: |- + # Anthropic Claude MCP Server + + **MCP Server Summary:** + Remote MCP server for Claude AI integration. + + ## Features + - Multi-turn conversations with Claude + - Tool use and function calling + - Computer use capabilities (beta) + + ## Note + This server is hosted by Anthropic. API key required for access. + deploymentMode: remote + endpoints: + sse: https://api.anthropic.com/v1/mcp/events + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.anthropic.com/mcp + repositoryUrl: https://github.com/anthropic/claude-mcp + sourceCode: anthropic/claude-mcp + publishedDate: "2025-01-18" + tools: + - name: chat + description: Send a message to Claude + accessType: read_only + parameters: + - name: message + type: string + description: User message + required: true + - name: model + type: string + description: Claude model to use (e.g., claude-3-opus) + required: false + - name: use_tool + description: Have Claude use a tool + accessType: read_write + parameters: + - name: tool_name + type: string + description: Name of the tool + required: true + - name: parameters + type: object + description: Tool parameters + required: true + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + claude: + metadataType: MetadataStringValue + string_value: "" + anthropic: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1737216000000" + lastUpdateTimeSinceEpoch: "1737216000000" + + - name: google-gemini-mcp + provider: Google + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Google Gemini AI integration through a managed remote MCP server. + Access multimodal capabilities, code generation, and reasoning. + readme: |- + # Google Gemini MCP Server + + **MCP Server Summary:** + Remote MCP server for Google Gemini AI. + + ## Features + - Multimodal input (text, images, video) + - Code generation and execution + - Advanced reasoning capabilities + + ## Note + Hosted on Google Cloud. API key required. + deploymentMode: remote + endpoints: + http: https://generativelanguage.googleapis.com/v1/mcp + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://ai.google.dev/docs + repositoryUrl: https://github.com/google/gemini-mcp + sourceCode: google/gemini-mcp + publishedDate: "2025-01-15" + tools: + - name: generate + description: Generate content with Gemini + accessType: read_only + parameters: + - name: prompt + type: string + description: Text prompt + required: true + - name: model + type: string + description: Model version (gemini-pro, gemini-ultra) + required: false + - name: analyze_image + description: Analyze an image with Gemini Vision + accessType: read_only + parameters: + - name: image_url + type: string + description: URL of the image to analyze + required: true + - name: prompt + type: string + description: Question about the image + required: true + customProperties: + ai: + metadataType: MetadataStringValue + string_value: "" + gemini: + metadataType: MetadataStringValue + string_value: "" + google: + metadataType: MetadataStringValue + string_value: "" + multimodal: + metadataType: MetadataStringValue + string_value: "" + remote: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736956800000" + lastUpdateTimeSinceEpoch: "1736956800000" diff --git a/manifests/kustomize/options/catalog/overlays/demo/dev-mcp-catalog-sources.yaml b/manifests/kustomize/options/catalog/overlays/demo/dev-mcp-catalog-sources.yaml new file mode 100644 index 0000000000..966ec09125 --- /dev/null +++ b/manifests/kustomize/options/catalog/overlays/demo/dev-mcp-catalog-sources.yaml @@ -0,0 +1,22 @@ +catalogs: + - name: "Organization MCP Servers" + id: organization_mcp_servers + type: yaml + enabled: true + properties: + yamlCatalogPath: dev-organization-mcp-servers.yaml + labels: + - Organization MCP + includedServers: + - "github-*" + - "slack-*" + - "jira-*" + - name: "Community and Custom MCP Servers" + id: community_mcp_servers + type: yaml + enabled: true + properties: + yamlCatalogPath: dev-community-mcp-servers.yaml + excludedServers: + - "*-deprecated" + - "*-experimental" diff --git a/manifests/kustomize/options/catalog/overlays/demo/dev-organization-mcp-servers.yaml b/manifests/kustomize/options/catalog/overlays/demo/dev-organization-mcp-servers.yaml new file mode 100644 index 0000000000..d67de6c75f --- /dev/null +++ b/manifests/kustomize/options/catalog/overlays/demo/dev-organization-mcp-servers.yaml @@ -0,0 +1,474 @@ +source: Organization MCP +mcp_servers: + - name: dynatrace-mcp + provider: Dynatrace + license: apache-2.0 + license_link: https://github.com/dynatrace-oss/dynatrace-mcp-server/blob/main/LICENSE + description: >- + Official Dynatrace-OSS project exposing DQL queries, problem feeds, and vulnerability data. + Gives agents real-time service health, letting them recommend rollbacks or capacity fixes inside OpenShift. + readme: |- + # Dynatrace MCP Server + + **MCP Server Summary:** + The Dynatrace MCP Server provides AI agents with access to observability data, enabling intelligent monitoring and analysis. + + ## Features + - Execute DQL queries for real-time observability + - Access problem feeds and incident information + - Retrieve vulnerability data from monitored services + + ## Prerequisites + - Dynatrace API token with appropriate permissions + - Dynatrace environment URL + + ## Installation + ```bash + npm install @dynatrace-oss/dynatrace-mcp-server + ``` + + ## Configuration + Set the following environment variables: + - `DYNATRACE_API_TOKEN`: Your Dynatrace API token + - `DYNATRACE_ENV_URL`: Your Dynatrace environment URL + version: "1.0.1" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.dynatrace.com/mcp + repositoryUrl: https://github.com/dynatrace-oss/dynatrace-mcp-server + sourceCode: dynatrace-oss/dynatrace-mcp-server + publishedDate: "2025-01-15" + tools: + - name: execute_dql + description: Execute Dynatrace Query Language (DQL) queries + accessType: read_only + parameters: + - name: query + type: string + description: DQL query to execute + required: true + - name: time_range + type: string + description: Time range for query (e.g., -1h, -24h) + required: false + - name: get_problems + description: Retrieve active problems and incidents + accessType: read_only + parameters: + - name: status + type: string + description: "Filter by status: OPEN, RESOLVED, or ALL" + required: false + - name: get_vulnerabilities + description: Query security vulnerabilities in monitored services + accessType: read_only + parameters: + - name: severity + type: string + description: "Minimum severity: CRITICAL, HIGH, MEDIUM, LOW" + required: false + artifacts: + - uri: oci://ghcr.io/dynatrace-oss/dynatrace-mcp-server:1.0.1 + createTimeSinceEpoch: "1736942400000" + lastUpdateTimeSinceEpoch: "1736942400000" + customProperties: + observability: + metadataType: MetadataStringValue + string_value: "" + monitoring: + metadataType: MetadataStringValue + string_value: "" + apm: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: true + createTimeSinceEpoch: "1736942400000" + lastUpdateTimeSinceEpoch: "1736942400000" + + - name: github-mcp + provider: GitHub + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Access GitHub repositories, issues, pull requests, and actions. + Search code, create issues, and manage workflows. + readme: |- + # GitHub MCP Server + + **MCP Server Summary:** + Full GitHub integration for AI agents. + + ## Features + - Code search across repositories + - Issue management + - Pull request operations + + ## Prerequisites + - GitHub Personal Access Token + - Appropriate repository permissions + version: "2.1.0" + transports: + - stdio + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.github.com/mcp + repositoryUrl: https://github.com/github/github-mcp + sourceCode: github/github-mcp + publishedDate: "2025-01-14" + tools: + - name: search_code + description: Search for code across repositories + accessType: read_only + parameters: + - name: query + type: string + description: Search query + required: true + - name: get_issues + description: List issues for a repository + accessType: read_only + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + - name: create_issue + description: Create a new issue + accessType: read_write + revoked: true + revokedReason: "Security vulnerability CVE-2025-1234 - write operations temporarily disabled pending patch" + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + - name: title + type: string + description: Issue title + required: true + - name: get_pull_requests + description: List pull requests + accessType: read_only + parameters: + - name: owner + type: string + description: Repository owner + required: true + - name: repo + type: string + description: Repository name + required: true + artifacts: + - uri: oci://ghcr.io/github/github-mcp:2.1.0 + createTimeSinceEpoch: "1736856000000" + lastUpdateTimeSinceEpoch: "1736856000000" + customProperties: + git: + metadataType: MetadataStringValue + string_value: "" + version-control: + metadataType: MetadataStringValue + string_value: "" + ci-cd: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736856000000" + lastUpdateTimeSinceEpoch: "1736856000000" + + - name: slack-mcp + provider: Slack Technologies + license: mit + license_link: https://opensource.org/licenses/MIT + description: >- + Send messages, manage channels, and integrate with Slack workspaces. + Perfect for automated notifications and team communication. + readme: |- + # Slack MCP Server + + **MCP Server Summary:** + Slack workspace integration for AI agents. + + ## Features + - Send messages to channels + - List channels + - Retrieve message history + + ## Prerequisites + - Slack Bot Token + - Appropriate workspace permissions + version: "1.5.0" + transports: + - sse + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + repositoryUrl: https://github.com/slack/slack-mcp + sourceCode: slack/slack-mcp + publishedDate: "2025-01-08" + tools: + - name: send_message + description: Send a message to a channel + accessType: read_write + revoked: true + revokedReason: "Rate limiting issues detected - tool temporarily disabled while investigating" + parameters: + - name: channel + type: string + description: Channel ID or name + required: true + - name: text + type: string + description: Message text + required: true + - name: list_channels + description: List available channels + accessType: read_only + parameters: [] + - name: get_channel_history + description: Get message history for a channel + accessType: read_only + parameters: + - name: channel + type: string + description: Channel ID + required: true + artifacts: + - uri: oci://ghcr.io/slack/slack-mcp:1.5.0 + createTimeSinceEpoch: "1736337600000" + lastUpdateTimeSinceEpoch: "1736337600000" + customProperties: + messaging: + metadataType: MetadataStringValue + string_value: "" + collaboration: + metadataType: MetadataStringValue + string_value: "" + notifications: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: false + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736337600000" + lastUpdateTimeSinceEpoch: "1736337600000" + + - name: jira-mcp + provider: Atlassian + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Manage Jira issues, projects, and workflows. + Create, update, and query issues using natural language. + readme: |- + # Jira MCP Server + + **MCP Server Summary:** + Jira project management integration. + + ## Features + - JQL search + - Issue creation and updates + - Workflow management + + ## Prerequisites + - Jira API token + - Site URL configuration + version: "1.3.2" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://developer.atlassian.com/mcp + repositoryUrl: https://github.com/atlassian/jira-mcp + sourceCode: atlassian/jira-mcp + publishedDate: "2025-01-11" + tools: + - name: search_issues + description: Search issues using JQL + accessType: read_only + parameters: + - name: jql + type: string + description: JQL query + required: true + - name: get_issue + description: Get issue details + accessType: read_only + parameters: + - name: issue_key + type: string + description: Issue key (e.g., PROJ-123) + required: true + - name: create_issue + description: Create a new issue + accessType: read_write + parameters: + - name: project + type: string + description: Project key + required: true + - name: summary + type: string + description: Issue summary + required: true + - name: issue_type + type: string + description: Issue type + required: true + - name: update_issue + description: Update an existing issue + accessType: read_write + parameters: + - name: issue_key + type: string + description: Issue key + required: true + artifacts: + - uri: oci://ghcr.io/atlassian/jira-mcp:1.3.2 + createTimeSinceEpoch: "1736596800000" + lastUpdateTimeSinceEpoch: "1736596800000" + customProperties: + project-management: + metadataType: MetadataStringValue + string_value: "" + issue-tracking: + metadataType: MetadataStringValue + string_value: "" + agile: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736596800000" + lastUpdateTimeSinceEpoch: "1736596800000" + + - name: aws-mcp + provider: Amazon Web Services + license: apache-2.0 + license_link: https://www.apache.org/licenses/LICENSE-2.0 + description: >- + Interact with Amazon Web Services resources. + Manage EC2 instances, S3 buckets, Lambda functions, and more. + readme: |- + # AWS MCP Server + + **MCP Server Summary:** + Amazon Web Services resource management. + + ## Features + - EC2 instance management + - S3 bucket operations + - Lambda invocation + - CloudWatch monitoring + + ## Prerequisites + - AWS credentials configured + - Appropriate IAM permissions + version: "2.0.1" + transports: + - http + logo: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDM4IDM4Ij4KICA8dGl0bGU+TUNQIFNlcnZlciBpY29uPC90aXRsZT4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmJne2ZpbGw6I2ZmZjt9CiAgICAgIC5ib3JkZXJ7ZmlsbDojZTBlMGUwO30KICAgICAgLmFjY2VudHtmaWxsOiMwMDY2Y2M7fQogICAgICAubm9kZXtmaWxsOiMwMDY2Y2M7fQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHJlY3QgY2xhc3M9ImJvcmRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiByeD0iNCIgcnk9IjQiLz4KICA8cmVjdCBjbGFzcz0iYmciIHg9IjEiIHk9IjEiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgcng9IjMiIHJ5PSIzIi8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjE5IiBjeT0iMTkiIHI9IjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMTkiIGN5PSI3IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iMjkiIGN5PSIxMyIgcj0iMi41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ibm9kZSIgY3g9IjI5IiBjeT0iMjUiIHI9IjIuNSIvPgogIDxjaXJjbGUgY2xhc3M9Im5vZGUiIGN4PSIxOSIgY3k9IjMxIiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjI1IiByPSIyLjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJub2RlIiBjeD0iOSIgY3k9IjEzIiByPSIyLjUiLz4KICA8bGluZSB4MT0iMTkiIHkxPSIxNCIgeDI9IjE5IiB5Mj0iOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjciIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIyMyIgeTE9IjIyIiB4Mj0iMjciIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxOSIgeTE9IjI0IiB4Mj0iMTkiIHkyPSIyOSIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjIyIiB4Mj0iMTEiIHkyPSIyNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxsaW5lIHgxPSIxNSIgeTE9IjE2IiB4Mj0iMTEiIHkyPSIxNCIgc3Ryb2tlPSIjMDA2NmNjIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8L3N2Zz4K + documentationUrl: https://docs.aws.amazon.com/mcp + repositoryUrl: https://github.com/aws/aws-mcp + sourceCode: aws/aws-mcp + publishedDate: "2025-01-09" + tools: + - name: list_ec2_instances + description: List EC2 instances + accessType: read_only + parameters: + - name: region + type: string + description: AWS region + required: true + - name: list_s3_buckets + description: List S3 buckets + accessType: read_only + parameters: [] + - name: invoke_lambda + description: Invoke a Lambda function + accessType: read_write + parameters: + - name: function_name + type: string + description: Lambda function name or ARN + required: true + - name: describe_cloudwatch_alarms + description: List CloudWatch alarms + accessType: read_only + parameters: [] + artifacts: + - uri: oci://public.ecr.aws/aws/aws-mcp:2.0.1 + createTimeSinceEpoch: "1736424000000" + lastUpdateTimeSinceEpoch: "1736424000000" + customProperties: + cloud: + metadataType: MetadataStringValue + string_value: "" + aws: + metadataType: MetadataStringValue + string_value: "" + infrastructure: + metadataType: MetadataStringValue + string_value: "" + verifiedSource: + metadataType: MetadataBoolValue + bool_value: true + secureEndpoint: + metadataType: MetadataBoolValue + bool_value: true + sast: + metadataType: MetadataBoolValue + bool_value: true + readOnlyTools: + metadataType: MetadataBoolValue + bool_value: false + createTimeSinceEpoch: "1736424000000" + lastUpdateTimeSinceEpoch: "1736424000000" diff --git a/manifests/kustomize/options/catalog/overlays/demo/kustomization.yaml b/manifests/kustomize/options/catalog/overlays/demo/kustomization.yaml index 8763ef25eb..37aa217b55 100644 --- a/manifests/kustomize/options/catalog/overlays/demo/kustomization.yaml +++ b/manifests/kustomize/options/catalog/overlays/demo/kustomization.yaml @@ -20,6 +20,14 @@ configMapGenerator: name: model-catalog-demo-perf-data options: disableNameSuffixHash: true +- behavior: create + files: + - mcp-sources.yaml=dev-mcp-catalog-sources.yaml + - dev-organization-mcp-servers.yaml=dev-organization-mcp-servers.yaml + - dev-community-mcp-servers.yaml=dev-community-mcp-servers.yaml + name: mcp-catalog-sources + options: + disableNameSuffixHash: true patches: - target: @@ -39,14 +47,28 @@ patches: value: name: perf-data emptyDir: {} + - op: add + path: /spec/template/spec/volumes/2 + value: + name: mcp-catalog-sources + configMap: + name: mcp-catalog-sources - op: add path: /spec/template/spec/containers/0/volumeMounts/0 value: name: perf-data mountPath: /perf-data + - op: add + path: /spec/template/spec/containers/0/volumeMounts/1 + value: + name: mcp-catalog-sources + mountPath: /mcp-catalog - op: add path: /spec/template/spec/containers/0/args/0 value: "--performance-metrics=/perf-data" + - op: add + path: /spec/template/spec/containers/0/args/1 + value: "--mcp-catalogs-path=/mcp-catalog/mcp-sources.yaml" - op: add path: /spec/template/spec/initContainers value: @@ -63,6 +85,8 @@ patches: mountPath: /demo-perf-data securityContext: allowPrivilegeEscalation: false + runAsNonRoot: false + runAsUser: 0 capabilities: drop: - ALL
+ {server.description || 'No description'} +
No MCP server card