Revert "feat(mcp): add MCP extension support with routes and configur… (#3166)

Revert "feat(mcp): add MCP extension support with routes and configuration"

This reverts commit 56afa6bd42.

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
This commit is contained in:
Ramkumar Chinchani
2025-05-22 21:29:00 -07:00
committed by GitHub
parent 56afa6bd42
commit eec57e62ca
7 changed files with 0 additions and 653 deletions
-4
View File
@@ -496,10 +496,6 @@ func (c *Config) IsMgmtEnabled() bool {
return c.IsSearchEnabled()
}
func (c *Config) IsMCPEnabled() bool {
return c.Extensions != nil && c.Extensions.MCP != nil && *c.Extensions.MCP.Enable
}
func (c *Config) IsImageTrustEnabled() bool {
return c.Extensions != nil && c.Extensions.Trust != nil && *c.Extensions.Trust.Enable
}
-5
View File
@@ -33,9 +33,4 @@ const (
UserPrefs = "/userprefs"
ExtUserPrefs = ExtPrefix + UserPrefs
FullUserPrefs = RoutePrefix + ExtUserPrefs
// mcp extension.
MCP = "/mcp"
ExtMCP = ExtPrefix + MCP
FullMCP = RoutePrefix + ExtMCP
)
-1
View File
@@ -212,7 +212,6 @@ func (rh *RouteHandler) SetupRoutes() {
rh.c.Log)
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
ext.SetupMCPRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.Log)
-5
View File
@@ -23,7 +23,6 @@ type ExtensionConfig struct {
APIKey *APIKeyConfig
Trust *ImageTrustConfig
Events *events.Config
MCP *MCPConfig
}
type ImageTrustConfig struct {
@@ -40,10 +39,6 @@ type MgmtConfig struct {
BaseConfig `mapstructure:",squash"`
}
type MCPConfig struct {
BaseConfig `mapstructure:",squash"`
}
type LintConfig struct {
BaseConfig `mapstructure:",squash"`
MandatoryAnnotations []string
-610
View File
@@ -1,610 +0,0 @@
//go:build mcp
// +build mcp
package extensions
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"zotregistry.dev/zot/pkg/api/config"
"zotregistry.dev/zot/pkg/api/constants"
"zotregistry.dev/zot/pkg/extensions/search/gql_generated"
zcommon "zotregistry.dev/zot/pkg/common"
"zotregistry.dev/zot/pkg/log"
mTypes "zotregistry.dev/zot/pkg/meta/types"
"zotregistry.dev/zot/pkg/storage"
)
func IsBuiltWithMCPExtension() bool {
return true
}
// MCP (Model Context Protocol) server implementation for zot registry
type MCPServer struct {
Conf *config.Config
Log log.Logger
MetaDB mTypes.MetaDB
StoreController storage.StoreController
}
// MCPResource represents a resource exposed via MCP
type MCPResource struct {
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description"`
MimeType string `json:"mimeType"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// MCPToolCall represents a tool call request
type MCPToolCall struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments,omitempty"`
}
// MCPToolResult represents a tool call result
type MCPToolResult struct {
Content []MCPContent `json:"content"`
IsError bool `json:"isError,omitempty"`
}
// MCPContent represents content in MCP responses
type MCPContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Data string `json:"data,omitempty"`
}
// MCPListResourcesResponse represents the response for listing resources
type MCPListResourcesResponse struct {
Resources []MCPResource `json:"resources"`
}
// MCPGetResourceResponse represents the response for getting a resource
type MCPGetResourceResponse struct {
Contents []MCPContent `json:"contents"`
}
// MCPListToolsResponse represents available tools
type MCPListToolsResponse struct {
Tools []MCPTool `json:"tools"`
}
// MCPTool represents an available MCP tool
type MCPTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]interface{} `json:"inputSchema"`
}
// SetupMCPRoutes sets up MCP extension routes
func SetupMCPRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, log log.Logger,
) {
log.Info().Msg("setting up MCP routes")
if !conf.IsSearchEnabled() {
log.Warn().Msg("MCP extension requires search extension to be enabled")
return
}
mcpServer := &MCPServer{
Conf: conf,
Log: log,
MetaDB: metaDB,
StoreController: storeController,
}
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
mcpRouter := router.PathPrefix(constants.ExtMCP).Subrouter()
mcpRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
mcpRouter.Use(zcommon.AddExtensionSecurityHeaders())
mcpRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
// MCP endpoints
mcpRouter.Methods(http.MethodGet).Path("/resources").HandlerFunc(mcpServer.ListResources)
mcpRouter.Methods(http.MethodGet).Path("/resources/{uri}").HandlerFunc(mcpServer.GetResource)
mcpRouter.Methods(http.MethodGet).Path("/tools").HandlerFunc(mcpServer.ListTools)
mcpRouter.Methods(http.MethodPost).Path("/tools/{name}").HandlerFunc(mcpServer.CallTool)
log.Info().Msg("finished setting up MCP routes")
}
// ListResources lists all available MCP resources
func (mcp *MCPServer) ListResources(w http.ResponseWriter, r *http.Request) {
resources := []MCPResource{
{
URI: "registry://repositories",
Name: "Registry Repositories",
Description: "List of all repositories in the registry",
MimeType: "application/json",
Metadata: map[string]string{
"type": "repository_list",
},
},
{
URI: "registry://images",
Name: "Registry Images",
Description: "List of all images in the registry",
MimeType: "application/json",
Metadata: map[string]string{
"type": "image_list",
},
},
{
URI: "registry://vulnerabilities",
Name: "CVE Information",
Description: "Vulnerability information for registry images",
MimeType: "application/json",
Metadata: map[string]string{
"type": "cve_list",
},
},
{
URI: "registry://annotations",
Name: "Image Annotations",
Description: "Annotations and metadata for registry images",
MimeType: "application/json",
Metadata: map[string]string{
"type": "annotation_list",
},
},
}
response := MCPListResourcesResponse{
Resources: resources,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// GetResource retrieves a specific MCP resource
func (mcp *MCPServer) GetResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uri := vars["uri"]
var contents []MCPContent
var err error
switch uri {
case "registry://repositories":
contents, err = mcp.getRepositoriesResource()
case "registry://images":
contents, err = mcp.getImagesResource()
case "registry://vulnerabilities":
contents, err = mcp.getVulnerabilitiesResource()
case "registry://annotations":
contents, err = mcp.getAnnotationsResource()
default:
http.Error(w, "Resource not found", http.StatusNotFound)
return
}
if err != nil {
mcp.Log.Error().Err(err).Str("uri", uri).Msg("failed to get MCP resource")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
response := MCPGetResourceResponse{
Contents: contents,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// ListTools lists available MCP tools
func (mcp *MCPServer) ListTools(w http.ResponseWriter, r *http.Request) {
tools := []MCPTool{
{
Name: "search_repositories",
Description: "Search for repositories in the registry",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "Search query string",
},
"limit": map[string]interface{}{
"type": "integer",
"description": "Maximum number of results to return",
"default": 10,
},
},
"required": []string{"query"},
},
},
{
Name: "search_images",
Description: "Search for images in the registry",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "Search query string",
},
"tag": map[string]interface{}{
"type": "string",
"description": "Filter by specific tag",
},
"limit": map[string]interface{}{
"type": "integer",
"description": "Maximum number of results to return",
"default": 10,
},
},
"required": []string{"query"},
},
},
{
Name: "get_cve_info",
Description: "Get CVE information for a specific image",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"repository": map[string]interface{}{
"type": "string",
"description": "Repository name",
},
"tag": map[string]interface{}{
"type": "string",
"description": "Image tag",
},
},
"required": []string{"repository", "tag"},
},
},
}
response := MCPListToolsResponse{
Tools: tools,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// CallTool handles MCP tool calls
func (mcp *MCPServer) CallTool(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
toolName := vars["name"]
var toolCall MCPToolCall
if err := json.NewDecoder(r.Body).Decode(&toolCall); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
var result MCPToolResult
var err error
switch toolName {
case "search_repositories":
result, err = mcp.handleSearchRepositories(toolCall.Arguments)
case "search_images":
result, err = mcp.handleSearchImages(toolCall.Arguments)
case "get_cve_info":
result, err = mcp.handleGetCVEInfo(toolCall.Arguments)
default:
result = MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Tool not found",
}},
IsError: true,
}
}
if err != nil {
mcp.Log.Error().Err(err).Str("tool", toolName).Msg("failed to execute MCP tool")
result = MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Internal server error: " + err.Error(),
}},
IsError: true,
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// Helper methods for getting resources
func (mcp *MCPServer) getRepositoriesResource() ([]MCPContent, error) {
if mcp.MetaDB == nil {
return []MCPContent{{
Type: "text",
Text: "MetaDB not available - search extension may not be enabled",
}}, nil
}
// Get repositories from MetaDB
repoMetas, err := mcp.MetaDB.GetAllRepoNames()
if err != nil {
return nil, err
}
data, err := json.MarshalIndent(map[string]interface{}{
"repositories": repoMetas,
"count": len(repoMetas),
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return nil, err
}
return []MCPContent{{
Type: "text",
Text: string(data),
}}, nil
}
func (mcp *MCPServer) getImagesResource() ([]MCPContent, error) {
if mcp.MetaDB == nil {
return []MCPContent{{
Type: "text",
Text: "MetaDB not available - search extension may not be enabled",
}}, nil
}
// Get basic image information
repoMetas, err := mcp.MetaDB.GetAllRepoNames()
if err != nil {
return nil, err
}
var imageList []map[string]interface{}
for _, repoName := range repoMetas {
// For each repo, get basic tag information
repoMeta, err := mcp.MetaDB.GetRepoMeta(r.Context(), repoName)
if err != nil {
continue
}
for tag := range repoMeta.Tags {
imageList = append(imageList, map[string]interface{}{
"repository": repoName,
"tag": tag,
})
}
}
data, err := json.MarshalIndent(map[string]interface{}{
"images": imageList,
"count": len(imageList),
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return nil, err
}
return []MCPContent{{
Type: "text",
Text: string(data),
}}, nil
}
func (mcp *MCPServer) getVulnerabilitiesResource() ([]MCPContent, error) {
// Placeholder for CVE information
// In a real implementation, this would integrate with the CVE scanner
data, err := json.MarshalIndent(map[string]interface{}{
"message": "CVE scanning requires additional configuration",
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return nil, err
}
return []MCPContent{{
Type: "text",
Text: string(data),
}}, nil
}
func (mcp *MCPServer) getAnnotationsResource() ([]MCPContent, error) {
// Placeholder for annotations
data, err := json.MarshalIndent(map[string]interface{}{
"message": "Annotations information available through image metadata",
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return nil, err
}
return []MCPContent{{
Type: "text",
Text: string(data),
}}, nil
}
// Helper methods for tool calls
func (mcp *MCPServer) handleSearchRepositories(args map[string]interface{}) (MCPToolResult, error) {
query, ok := args["query"].(string)
if !ok {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Query parameter is required",
}},
IsError: true,
}, nil
}
limit := 10
if l, ok := args["limit"].(float64); ok {
limit = int(l)
}
if mcp.MetaDB == nil {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Search not available - MetaDB not initialized",
}},
IsError: true,
}, nil
}
// Search repositories
repoMetas, err := mcp.MetaDB.SearchRepos(r.Context(), query)
if err != nil {
return MCPToolResult{}, err
}
// Limit results
if len(repoMetas) > limit {
repoMetas = repoMetas[:limit]
}
// Convert to simple format
var results []map[string]interface{}
for _, repo := range repoMetas {
results = append(results, map[string]interface{}{
"name": repo.Name,
"lastUpdated": repo.LastUpdatedImage.LastUpdated,
"size": repo.Size,
"platforms": repo.Platforms,
})
}
data, err := json.MarshalIndent(map[string]interface{}{
"query": query,
"results": results,
"count": len(results),
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return MCPToolResult{}, err
}
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: string(data),
}},
}, nil
}
func (mcp *MCPServer) handleSearchImages(args map[string]interface{}) (MCPToolResult, error) {
query, ok := args["query"].(string)
if !ok {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Query parameter is required",
}},
IsError: true,
}, nil
}
limit := 10
if l, ok := args["limit"].(float64); ok {
limit = int(l)
}
if mcp.MetaDB == nil {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Search not available - MetaDB not initialized",
}},
IsError: true,
}, nil
}
// Search images
imageMetas, err := mcp.MetaDB.SearchTags(r.Context(), query)
if err != nil {
return MCPToolResult{}, err
}
// Limit results
if len(imageMetas) > limit {
imageMetas = imageMetas[:limit]
}
// Convert to simple format
var results []map[string]interface{}
for _, image := range imageMetas {
results = append(results, map[string]interface{}{
"repository": image.Repo,
"tag": image.Tag,
"digest": image.Digest,
"lastUpdated": image.LastUpdated,
"size": image.Size,
})
}
data, err := json.MarshalIndent(map[string]interface{}{
"query": query,
"results": results,
"count": len(results),
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return MCPToolResult{}, err
}
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: string(data),
}},
}, nil
}
func (mcp *MCPServer) handleGetCVEInfo(args map[string]interface{}) (MCPToolResult, error) {
repository, ok := args["repository"].(string)
if !ok {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Repository parameter is required",
}},
IsError: true,
}, nil
}
tag, ok := args["tag"].(string)
if !ok {
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: "Tag parameter is required",
}},
IsError: true,
}, nil
}
// Placeholder for CVE information
// In a real implementation, this would integrate with the CVE scanner
data, err := json.MarshalIndent(map[string]interface{}{
"repository": repository,
"tag": tag,
"message": "CVE scanning requires CVE scanner to be configured and enabled",
"timestamp": time.Now().Format(time.RFC3339),
}, "", " ")
if err != nil {
return MCPToolResult{}, err
}
return MCPToolResult{
Content: []MCPContent{{
Type: "text",
Text: string(data),
}},
}, nil
}
-24
View File
@@ -1,24 +0,0 @@
//go:build !mcp
// +build !mcp
package extensions
import (
"github.com/gorilla/mux"
"zotregistry.dev/zot/pkg/api/config"
"zotregistry.dev/zot/pkg/log"
mTypes "zotregistry.dev/zot/pkg/meta/types"
"zotregistry.dev/zot/pkg/storage"
)
func IsBuiltWithMCPExtension() bool {
return false
}
func SetupMCPRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, log log.Logger,
) {
log.Warn().Msg("skipping setting up MCP routes because given zot binary " +
"doesn't include this feature, please build a binary that does so")
}
-4
View File
@@ -36,10 +36,6 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
endpoints = append(endpoints, constants.FullMgmt)
}
if config.IsMCPEnabled() && IsBuiltWithMCPExtension() {
endpoints = append(endpoints, constants.FullMCP)
}
if len(endpoints) > 0 {
extensions = append(extensions, distext.Extension{
Name: constants.BaseExtension,