yao/registry/manager/mcp/add.go
Max 44af6ba759 Enhance command structure with MCP and Robot functionalities
- Introduce new MCP and Robot command groups in the CLI, allowing for better organization of package management commands.
- Add corresponding command descriptions for MCP and Robot functionalities to improve user guidance.
- Update the agent command group to include additional commands for enhanced agent management.
- Modify .gitignore to exclude specific design markdown files from version control.
- Add environment variable for test application path in GitHub workflows to streamline testing setup.
2026-03-03 08:11:43 +08:00

151 lines
3.9 KiB
Go

package mcp
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/yaoapp/yao/registry/manager/common"
)
// AddOptions configures the Add operation.
type AddOptions struct {
Version string
Force bool
}
// Add installs an MCP package from the registry.
// Per DESIGN-MCP.md:
// 1. Check conflict
// 2. Pull from registry
// 3. Check dependencies (MCPs currently have none, but structure supports it)
// 4. Unpack .mcp.yao + mapping/ to mcps/{scope}/{name}/
// 5. Extract scripts/ to project root scripts/{scope}/
// 6. Check script conflicts
// 7. Write registry.yao (files include both MCP dir and scripts)
// 8. Hot-reload
func (m *Manager) Add(pkgID string, opts AddOptions) error {
if opts.Version == "" {
opts.Version = "latest"
}
scope, name, err := common.ParsePackageID(pkgID)
if err != nil {
return err
}
lf, err := common.LoadLockfile(m.appRoot)
if err != nil {
return err
}
if existing, ok := lf.GetPackage(pkgID); ok && !opts.Force {
return fmt.Errorf("package %s is already installed (version %s). Use --force to reinstall", pkgID, existing.Version)
}
destDir := common.PackageDir(common.TypeMCP, scope, name, m.appRoot)
if _, err := os.Stat(destDir); err == nil {
if _, ok := lf.GetPackage(pkgID); !ok {
return fmt.Errorf("directory %s already exists but is not managed by registry. Please remove or relocate it first", destDir)
}
}
regType := common.TypeToRegistryType(common.TypeMCP)
zipData, digest, err := m.client.Pull(regType, "@"+scope, name, opts.Version)
if err != nil {
return fmt.Errorf("pull %s: %w", pkgID, err)
}
manifest, err := common.ReadManifest(zipData)
if err != nil {
return fmt.Errorf("read manifest: %w", err)
}
// Unpack everything to a temp dir first, then sort into MCP dir and scripts
tempDir, err := os.MkdirTemp("", "yao-mcp-install-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
allFiles, err := common.UnpackTo(zipData, tempDir)
if err != nil {
return fmt.Errorf("unpack: %w", err)
}
fileHashes := map[string]string{}
mcpRelDir := common.PackageDirRel(common.TypeMCP, scope, name)
for _, f := range allFiles {
srcPath := filepath.Join(tempDir, f)
if strings.HasPrefix(f, "scripts/") {
// Script file → project root scripts/
destPath := filepath.Join(m.appRoot, f)
// Check script conflict: exists but not in registry.yao
if _, err := os.Stat(destPath); err == nil {
if !isScriptTracked(lf, f) {
return fmt.Errorf("script file %s already exists and is not managed by registry. Please remove or relocate it first", f)
}
}
if err := copyFileFromTo(srcPath, destPath); err != nil {
return err
}
hash, _ := common.HashFile(destPath)
fileHashes[f] = hash
} else {
// MCP file → mcps/{scope}/{name}/
destPath := filepath.Join(destDir, f)
if err := copyFileFromTo(srcPath, destPath); err != nil {
return err
}
relPath := mcpRelDir + "/" + f
hash, _ := common.HashFile(destPath)
fileHashes[relPath] = hash
}
}
info := common.PackageInfo{
Type: common.TypeMCP,
Version: manifest.Version,
Integrity: digest,
Dependencies: manifest.Dependencies,
Files: fileHashes,
}
lf.SetPackage(pkgID, info)
for depID := range manifest.Dependencies {
lf.AddRequiredBy(depID, pkgID)
}
if err := common.SaveLockfile(m.appRoot, lf); err != nil {
return err
}
fmt.Printf("✓ Installed %s@%s → %s\n", pkgID, manifest.Version, destDir)
return nil
}
// isScriptTracked checks if a script path is tracked by any package in the lockfile.
func isScriptTracked(lf *common.RegistryYao, scriptPath string) bool {
for _, pkg := range lf.Packages {
if _, ok := pkg.Files[scriptPath]; ok {
return true
}
}
return false
}
func copyFileFromTo(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}