899 lines
23 KiB
Go
899 lines
23 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/format"
|
|
"github.com/ollama/ollama/progress"
|
|
"github.com/ollama/ollama/server"
|
|
"github.com/ollama/ollama/types/model"
|
|
)
|
|
|
|
// MCPConfigFile represents the global MCP configuration file structure.
|
|
type MCPConfigFile struct {
|
|
MCPServers map[string]MCPServerConfig `json:"mcpServers"`
|
|
}
|
|
|
|
// MCPServerConfig represents a single MCP server configuration.
|
|
type MCPServerConfig struct {
|
|
Type string `json:"type,omitempty"`
|
|
Command string `json:"command"`
|
|
Args []string `json:"args,omitempty"`
|
|
Env map[string]string `json:"env,omitempty"`
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
}
|
|
|
|
// getMCPConfigPath returns the path to the global MCP config file.
|
|
func getMCPConfigPath() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Join(home, ".ollama", "mcp.json")
|
|
}
|
|
|
|
// loadMCPConfig loads the global MCP configuration file.
|
|
func loadMCPConfig() (*MCPConfigFile, error) {
|
|
configPath := getMCPConfigPath()
|
|
if configPath == "" {
|
|
return nil, fmt.Errorf("could not determine home directory")
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// Return empty config if file doesn't exist
|
|
return &MCPConfigFile{
|
|
MCPServers: make(map[string]MCPServerConfig),
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("reading config: %w", err)
|
|
}
|
|
|
|
var config MCPConfigFile
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("parsing config: %w", err)
|
|
}
|
|
|
|
if config.MCPServers == nil {
|
|
config.MCPServers = make(map[string]MCPServerConfig)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// saveMCPConfig saves the global MCP configuration file.
|
|
func saveMCPConfig(config *MCPConfigFile) error {
|
|
configPath := getMCPConfigPath()
|
|
if configPath == "" {
|
|
return fmt.Errorf("could not determine home directory")
|
|
}
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
|
return fmt.Errorf("creating config directory: %w", err)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling config: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, data, 0o644); err != nil {
|
|
return fmt.Errorf("writing config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPAddHandler handles the mcp add command.
|
|
func MCPAddHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 2 {
|
|
return fmt.Errorf("usage: ollama mcp add NAME COMMAND [ARGS...]")
|
|
}
|
|
|
|
name := args[0]
|
|
command := args[1]
|
|
cmdArgs := args[2:]
|
|
|
|
// Load existing config
|
|
config, err := loadMCPConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
// Check if already exists
|
|
if _, exists := config.MCPServers[name]; exists {
|
|
fmt.Fprintf(os.Stderr, "Warning: overwriting existing MCP server '%s'\n", name)
|
|
}
|
|
|
|
// Add the new server
|
|
config.MCPServers[name] = MCPServerConfig{
|
|
Type: "stdio",
|
|
Command: command,
|
|
Args: cmdArgs,
|
|
}
|
|
|
|
// Save config
|
|
if err := saveMCPConfig(config); err != nil {
|
|
return fmt.Errorf("saving config: %w", err)
|
|
}
|
|
|
|
configPath := getMCPConfigPath()
|
|
fmt.Fprintf(os.Stderr, "Added MCP server '%s' to %s\n", name, configPath)
|
|
fmt.Fprintf(os.Stderr, " Command: %s %s\n", command, strings.Join(cmdArgs, " "))
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPRemoveGlobalHandler handles removing an MCP from global config.
|
|
func MCPRemoveGlobalHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: ollama mcp remove-global NAME [NAME...]")
|
|
}
|
|
|
|
config, err := loadMCPConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
for _, name := range args {
|
|
if _, exists := config.MCPServers[name]; !exists {
|
|
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
|
|
continue
|
|
}
|
|
|
|
delete(config.MCPServers, name)
|
|
fmt.Fprintf(os.Stderr, "Removed MCP server '%s' from global config\n", name)
|
|
}
|
|
|
|
if err := saveMCPConfig(config); err != nil {
|
|
return fmt.Errorf("saving config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPListGlobalHandler handles listing global MCP servers.
|
|
func MCPListGlobalHandler(cmd *cobra.Command, args []string) error {
|
|
config, err := loadMCPConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
if len(config.MCPServers) == 0 {
|
|
fmt.Println("No global MCP servers configured")
|
|
fmt.Printf("Add one with: ollama mcp add NAME COMMAND [ARGS...]\n")
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Global MCP servers (%s):\n\n", getMCPConfigPath())
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tCOMMAND\tSTATUS")
|
|
|
|
for name, srv := range config.MCPServers {
|
|
cmdLine := srv.Command
|
|
if len(srv.Args) > 0 {
|
|
cmdLine += " " + strings.Join(srv.Args, " ")
|
|
}
|
|
status := "enabled"
|
|
if srv.Disabled {
|
|
status = "disabled"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n", name, cmdLine, status)
|
|
}
|
|
|
|
return w.Flush()
|
|
}
|
|
|
|
// MCPDisableHandler handles disabling an MCP server in global config.
|
|
func MCPDisableHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: ollama mcp disable NAME [NAME...]")
|
|
}
|
|
|
|
config, err := loadMCPConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
for _, name := range args {
|
|
srv, exists := config.MCPServers[name]
|
|
if !exists {
|
|
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
|
|
continue
|
|
}
|
|
|
|
if srv.Disabled {
|
|
fmt.Fprintf(os.Stderr, "MCP server '%s' is already disabled\n", name)
|
|
continue
|
|
}
|
|
|
|
srv.Disabled = true
|
|
config.MCPServers[name] = srv
|
|
fmt.Fprintf(os.Stderr, "Disabled MCP server '%s'\n", name)
|
|
}
|
|
|
|
if err := saveMCPConfig(config); err != nil {
|
|
return fmt.Errorf("saving config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPEnableHandler handles enabling an MCP server in global config.
|
|
func MCPEnableHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: ollama mcp enable NAME [NAME...]")
|
|
}
|
|
|
|
config, err := loadMCPConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
for _, name := range args {
|
|
srv, exists := config.MCPServers[name]
|
|
if !exists {
|
|
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
|
|
continue
|
|
}
|
|
|
|
if !srv.Disabled {
|
|
fmt.Fprintf(os.Stderr, "MCP server '%s' is already enabled\n", name)
|
|
continue
|
|
}
|
|
|
|
srv.Disabled = false
|
|
config.MCPServers[name] = srv
|
|
fmt.Fprintf(os.Stderr, "Enabled MCP server '%s'\n", name)
|
|
}
|
|
|
|
if err := saveMCPConfig(config); err != nil {
|
|
return fmt.Errorf("saving config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPPushHandler handles the mcp push command.
|
|
func MCPPushHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 2 {
|
|
return fmt.Errorf("usage: ollama mcp push NAME[:TAG] PATH")
|
|
}
|
|
|
|
name := args[0]
|
|
path := args[1]
|
|
|
|
// Expand path
|
|
if strings.HasPrefix(path, "~") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("expanding home directory: %w", err)
|
|
}
|
|
path = filepath.Join(home, path[1:])
|
|
}
|
|
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return fmt.Errorf("resolving path: %w", err)
|
|
}
|
|
|
|
// Validate MCP directory - check for mcp.json, package.json, or any config file
|
|
validFiles := []string{"mcp.json", "package.json", "server.py", "server.js", "main.py", "index.js"}
|
|
found := false
|
|
for _, vf := range validFiles {
|
|
if _, err := os.Stat(filepath.Join(absPath, vf)); err == nil {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("MCP directory should contain one of: %s", strings.Join(validFiles, ", "))
|
|
}
|
|
|
|
// Parse MCP name (will set Kind="mcp")
|
|
n := server.ParseMCPName(name)
|
|
if n.Model == "" {
|
|
return fmt.Errorf("invalid MCP name: %s", name)
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.Stop()
|
|
|
|
// Create MCP layer
|
|
displayName := n.DisplayShortest()
|
|
status := fmt.Sprintf("Creating MCP layer for %s", displayName)
|
|
spinner := progress.NewSpinner(status)
|
|
p.Add(status, spinner)
|
|
|
|
layer, err := server.CreateMCPLayer(absPath)
|
|
if err != nil {
|
|
return fmt.Errorf("creating MCP layer: %w", err)
|
|
}
|
|
|
|
spinner.Stop()
|
|
|
|
// Create MCP manifest
|
|
manifest, configLayer, err := createMCPManifest(absPath, layer)
|
|
if err != nil {
|
|
return fmt.Errorf("creating MCP manifest: %w", err)
|
|
}
|
|
|
|
// Write manifest locally
|
|
manifestPath, err := server.GetMCPManifestPath(n)
|
|
if err != nil {
|
|
return fmt.Errorf("getting manifest path: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {
|
|
return fmt.Errorf("creating manifest directory: %w", err)
|
|
}
|
|
|
|
manifestJSON, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling manifest: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(manifestPath, manifestJSON, 0o644); err != nil {
|
|
return fmt.Errorf("writing manifest: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "MCP %s created locally\n", displayName)
|
|
fmt.Fprintf(os.Stderr, " Config: %s (%s)\n", configLayer.Digest, format.HumanBytes(configLayer.Size))
|
|
fmt.Fprintf(os.Stderr, " Layer: %s (%s)\n", layer.Digest, format.HumanBytes(layer.Size))
|
|
|
|
// Push to registry
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return fmt.Errorf("creating client: %w", err)
|
|
}
|
|
|
|
insecure, _ := cmd.Flags().GetBool("insecure")
|
|
|
|
fmt.Fprintf(os.Stderr, "\nPushing to registry...\n")
|
|
|
|
fn := func(resp api.ProgressResponse) error {
|
|
if resp.Digest != "" {
|
|
bar := progress.NewBar(resp.Status, resp.Total, resp.Completed)
|
|
p.Add(resp.Digest, bar)
|
|
} else if resp.Status != "" {
|
|
spinner := progress.NewSpinner(resp.Status)
|
|
p.Add(resp.Status, spinner)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
req := &api.PushRequest{
|
|
Model: displayName,
|
|
Insecure: insecure,
|
|
}
|
|
|
|
if err := client.Push(context.Background(), req, fn); err != nil {
|
|
// If push fails, still show success for local creation
|
|
fmt.Fprintf(os.Stderr, "\nNote: Local MCP created but push failed: %v\n", err)
|
|
fmt.Fprintf(os.Stderr, "You can try pushing later with: ollama mcp push %s\n", name)
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "Successfully pushed %s\n", displayName)
|
|
return nil
|
|
}
|
|
|
|
// MCPPullHandler handles the mcp pull command.
|
|
func MCPPullHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("usage: ollama mcp pull NAME[:TAG]")
|
|
}
|
|
|
|
name := args[0]
|
|
n := server.ParseMCPName(name)
|
|
if n.Model == "" {
|
|
return fmt.Errorf("invalid MCP name: %s", name)
|
|
}
|
|
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return fmt.Errorf("creating client: %w", err)
|
|
}
|
|
|
|
insecure, _ := cmd.Flags().GetBool("insecure")
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.Stop()
|
|
|
|
fn := func(resp api.ProgressResponse) error {
|
|
if resp.Digest != "" {
|
|
bar := progress.NewBar(resp.Status, resp.Total, resp.Completed)
|
|
p.Add(resp.Digest, bar)
|
|
} else if resp.Status != "" {
|
|
spinner := progress.NewSpinner(resp.Status)
|
|
p.Add(resp.Status, spinner)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
displayName := n.DisplayShortest()
|
|
req := &api.PullRequest{
|
|
Model: displayName,
|
|
Insecure: insecure,
|
|
}
|
|
|
|
if err := client.Pull(context.Background(), req, fn); err != nil {
|
|
return fmt.Errorf("pulling MCP: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "Successfully pulled %s\n", displayName)
|
|
return nil
|
|
}
|
|
|
|
// MCPListHandler handles the mcp list command.
|
|
func MCPListHandler(cmd *cobra.Command, args []string) error {
|
|
mcps, err := listLocalMCPs()
|
|
if err != nil {
|
|
return fmt.Errorf("listing MCPs: %w", err)
|
|
}
|
|
|
|
if len(mcps) == 0 {
|
|
fmt.Println("No MCPs installed")
|
|
return nil
|
|
}
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tTAG\tSIZE\tMODIFIED")
|
|
|
|
for _, mcp := range mcps {
|
|
fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n",
|
|
mcp.Namespace,
|
|
mcp.Name,
|
|
mcp.Tag,
|
|
format.HumanBytes(mcp.Size),
|
|
format.HumanTime(mcp.ModifiedAt, "Never"),
|
|
)
|
|
}
|
|
|
|
return w.Flush()
|
|
}
|
|
|
|
// MCPRemoveHandler handles the mcp rm command.
|
|
func MCPRemoveHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: ollama mcp rm NAME[:TAG] [NAME[:TAG]...]")
|
|
}
|
|
|
|
for _, name := range args {
|
|
n := server.ParseMCPName(name)
|
|
if n.Model == "" {
|
|
fmt.Fprintf(os.Stderr, "Invalid MCP name: %s\n", name)
|
|
continue
|
|
}
|
|
|
|
displayName := n.DisplayShortest()
|
|
manifestPath, err := server.GetMCPManifestPath(n)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error getting manifest path for %s: %v\n", name, err)
|
|
continue
|
|
}
|
|
|
|
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
|
fmt.Fprintf(os.Stderr, "MCP not found: %s\n", displayName)
|
|
continue
|
|
}
|
|
|
|
if err := os.Remove(manifestPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error removing %s: %v\n", displayName, err)
|
|
continue
|
|
}
|
|
|
|
// Clean up empty parent directories
|
|
dir := filepath.Dir(manifestPath)
|
|
for dir != filepath.Join(os.Getenv("HOME"), ".ollama", "models", "manifests") {
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) == 0 {
|
|
os.Remove(dir)
|
|
dir = filepath.Dir(dir)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "Deleted '%s'\n", displayName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPShowHandler handles the mcp show command.
|
|
func MCPShowHandler(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("usage: ollama mcp show NAME[:TAG]")
|
|
}
|
|
|
|
name := args[0]
|
|
n := server.ParseMCPName(name)
|
|
if n.Model == "" {
|
|
return fmt.Errorf("invalid MCP name: %s", name)
|
|
}
|
|
|
|
displayName := n.DisplayShortest()
|
|
manifestPath, err := server.GetMCPManifestPath(n)
|
|
if err != nil {
|
|
return fmt.Errorf("getting manifest path: %w", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("MCP not found: %s", displayName)
|
|
}
|
|
return fmt.Errorf("reading manifest: %w", err)
|
|
}
|
|
|
|
var manifest server.Manifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return fmt.Errorf("parsing manifest: %w", err)
|
|
}
|
|
|
|
fmt.Printf("MCP: %s\n\n", displayName)
|
|
|
|
fmt.Println("Layers:")
|
|
for _, layer := range manifest.Layers {
|
|
fmt.Printf(" %s %s %s\n", layer.MediaType, layer.Digest[:19], format.HumanBytes(layer.Size))
|
|
}
|
|
|
|
// Try to read and display mcp.json or package.json content
|
|
if len(manifest.Layers) > 0 {
|
|
for _, layer := range manifest.Layers {
|
|
if layer.MediaType == server.MediaTypeMCP {
|
|
mcpPath, err := server.GetMCPsPath(layer.Digest)
|
|
if err == nil {
|
|
// Try mcp.json first
|
|
mcpJSONPath := filepath.Join(mcpPath, "mcp.json")
|
|
if content, err := os.ReadFile(mcpJSONPath); err == nil {
|
|
fmt.Println("\nConfig (mcp.json):")
|
|
fmt.Println(string(content))
|
|
} else {
|
|
// Try package.json
|
|
pkgJSONPath := filepath.Join(mcpPath, "package.json")
|
|
if content, err := os.ReadFile(pkgJSONPath); err == nil {
|
|
fmt.Println("\nConfig (package.json):")
|
|
fmt.Println(string(content))
|
|
}
|
|
}
|
|
|
|
// List files in the MCP
|
|
fmt.Println("\nFiles:")
|
|
filepath.Walk(mcpPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
relPath, _ := filepath.Rel(mcpPath, path)
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
fmt.Printf(" %s/\n", relPath)
|
|
} else {
|
|
fmt.Printf(" %s (%s)\n", relPath, format.HumanBytes(info.Size()))
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MCPInfo represents information about an installed MCP.
|
|
type MCPInfo struct {
|
|
Namespace string
|
|
Name string
|
|
Tag string
|
|
Size int64
|
|
ModifiedAt time.Time
|
|
}
|
|
|
|
// listLocalMCPs returns a list of locally installed MCPs.
|
|
// MCPs are stored with 5-part paths: host/namespace/kind/model/tag
|
|
// where kind is "mcp".
|
|
func listLocalMCPs() ([]MCPInfo, error) {
|
|
manifestsPath := filepath.Join(os.Getenv("HOME"), ".ollama", "models", "manifests")
|
|
|
|
var mcps []MCPInfo
|
|
|
|
// Walk through all registries
|
|
registries, err := os.ReadDir(manifestsPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return mcps, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
for _, registry := range registries {
|
|
if !registry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Walk namespaces
|
|
namespaces, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, namespace := range namespaces {
|
|
if !namespace.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Walk kinds looking for "mcp"
|
|
kinds, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, kind := range kinds {
|
|
if !kind.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Only process mcp kind
|
|
if kind.Name() != server.MCPNamespace {
|
|
continue
|
|
}
|
|
|
|
// Walk MCP names (model names)
|
|
mcpNames, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, mcpName := range mcpNames {
|
|
if !mcpName.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Walk tags
|
|
tags, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name(), mcpName.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, tag := range tags {
|
|
manifestPath := filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name(), mcpName.Name(), tag.Name())
|
|
fi, err := os.Stat(manifestPath)
|
|
if err != nil || fi.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Read manifest to get size
|
|
data, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var manifest server.Manifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
continue
|
|
}
|
|
|
|
var totalSize int64
|
|
for _, layer := range manifest.Layers {
|
|
totalSize += layer.Size
|
|
}
|
|
|
|
// Build display name using model.Name
|
|
n := model.Name{
|
|
Host: registry.Name(),
|
|
Namespace: namespace.Name(),
|
|
Kind: kind.Name(),
|
|
Model: mcpName.Name(),
|
|
Tag: tag.Name(),
|
|
}
|
|
|
|
mcps = append(mcps, MCPInfo{
|
|
Namespace: n.Namespace + "/" + n.Kind,
|
|
Name: n.Model,
|
|
Tag: n.Tag,
|
|
Size: totalSize,
|
|
ModifiedAt: fi.ModTime(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return mcps, nil
|
|
}
|
|
|
|
// createMCPManifest creates a manifest for a standalone MCP.
|
|
func createMCPManifest(mcpDir string, layer server.Layer) (*server.Manifest, *server.Layer, error) {
|
|
// Try to read mcp.json or package.json to extract metadata
|
|
name, description := extractMCPMetadata(mcpDir)
|
|
if name == "" {
|
|
// Use directory name as fallback
|
|
name = filepath.Base(mcpDir)
|
|
}
|
|
|
|
// Create config
|
|
config := map[string]any{
|
|
"name": name,
|
|
"description": description,
|
|
"architecture": "amd64",
|
|
"os": "linux",
|
|
}
|
|
|
|
configJSON, err := json.Marshal(config)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("marshaling config: %w", err)
|
|
}
|
|
|
|
// Create config layer
|
|
configLayer, err := server.NewLayer(strings.NewReader(string(configJSON)), "application/vnd.docker.container.image.v1+json")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("creating config layer: %w", err)
|
|
}
|
|
|
|
manifest := &server.Manifest{
|
|
SchemaVersion: 2,
|
|
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
|
Config: configLayer,
|
|
Layers: []server.Layer{layer},
|
|
}
|
|
|
|
return manifest, &configLayer, nil
|
|
}
|
|
|
|
// extractMCPMetadata extracts name and description from mcp.json or package.json.
|
|
func extractMCPMetadata(mcpDir string) (name, description string) {
|
|
// Try mcp.json first
|
|
mcpJSONPath := filepath.Join(mcpDir, "mcp.json")
|
|
if data, err := os.ReadFile(mcpJSONPath); err == nil {
|
|
var config map[string]any
|
|
if err := json.Unmarshal(data, &config); err == nil {
|
|
if n, ok := config["name"].(string); ok {
|
|
name = n
|
|
}
|
|
if d, ok := config["description"].(string); ok {
|
|
description = d
|
|
}
|
|
return name, description
|
|
}
|
|
}
|
|
|
|
// Try package.json
|
|
pkgJSONPath := filepath.Join(mcpDir, "package.json")
|
|
if data, err := os.ReadFile(pkgJSONPath); err == nil {
|
|
var config map[string]any
|
|
if err := json.Unmarshal(data, &config); err == nil {
|
|
if n, ok := config["name"].(string); ok {
|
|
name = n
|
|
}
|
|
if d, ok := config["description"].(string); ok {
|
|
description = d
|
|
}
|
|
return name, description
|
|
}
|
|
}
|
|
|
|
return "", ""
|
|
}
|
|
|
|
// NewMCPCommand creates the mcp parent command with subcommands.
|
|
func NewMCPCommand() *cobra.Command {
|
|
mcpCmd := &cobra.Command{
|
|
Use: "mcp",
|
|
Short: "Manage MCP servers",
|
|
Long: "Commands for managing MCP (Model Context Protocol) servers (add, push, pull, list, rm, show)",
|
|
}
|
|
|
|
// Global config commands
|
|
addCmd := &cobra.Command{
|
|
Use: "add NAME COMMAND [ARGS...]",
|
|
Short: "Add an MCP server to global config",
|
|
Long: `Add an MCP server to the global config (~/.ollama/mcp.json).
|
|
Global MCP servers are available to all agents.
|
|
|
|
Examples:
|
|
ollama mcp add web-search uv run ./mcp-server.py
|
|
ollama mcp add calculator python3 /path/to/calc.py`,
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: MCPAddHandler,
|
|
DisableFlagParsing: true, // Allow args with dashes
|
|
}
|
|
|
|
removeGlobalCmd := &cobra.Command{
|
|
Use: "remove-global NAME [NAME...]",
|
|
Aliases: []string{"rm-global"},
|
|
Short: "Remove an MCP server from global config",
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: MCPRemoveGlobalHandler,
|
|
}
|
|
|
|
listGlobalCmd := &cobra.Command{
|
|
Use: "list-global",
|
|
Short: "List global MCP servers",
|
|
Args: cobra.NoArgs,
|
|
RunE: MCPListGlobalHandler,
|
|
}
|
|
|
|
// Registry commands
|
|
pushCmd := &cobra.Command{
|
|
Use: "push NAME[:TAG] PATH",
|
|
Short: "Push an MCP server to a registry",
|
|
Long: "Package a local MCP server directory and push it to a registry",
|
|
Args: cobra.ExactArgs(2),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: MCPPushHandler,
|
|
}
|
|
pushCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
|
|
|
pullCmd := &cobra.Command{
|
|
Use: "pull NAME[:TAG]",
|
|
Short: "Pull an MCP server from a registry",
|
|
Args: cobra.ExactArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: MCPPullHandler,
|
|
}
|
|
pullCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
|
|
|
listCmd := &cobra.Command{
|
|
Use: "list",
|
|
Aliases: []string{"ls"},
|
|
Short: "List installed MCP servers (from registry)",
|
|
Args: cobra.NoArgs,
|
|
RunE: MCPListHandler,
|
|
}
|
|
|
|
rmCmd := &cobra.Command{
|
|
Use: "rm NAME[:TAG] [NAME[:TAG]...]",
|
|
Aliases: []string{"remove", "delete"},
|
|
Short: "Remove an MCP server (from registry)",
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: MCPRemoveHandler,
|
|
}
|
|
|
|
showCmd := &cobra.Command{
|
|
Use: "show NAME[:TAG]",
|
|
Short: "Show MCP server details",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: MCPShowHandler,
|
|
}
|
|
|
|
disableCmd := &cobra.Command{
|
|
Use: "disable NAME [NAME...]",
|
|
Short: "Disable an MCP server (keep in config)",
|
|
Long: `Disable an MCP server without removing it from config.
|
|
Disabled servers will not be started when running agents.
|
|
Use 'ollama mcp enable' to re-enable.`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: MCPDisableHandler,
|
|
}
|
|
|
|
enableCmd := &cobra.Command{
|
|
Use: "enable NAME [NAME...]",
|
|
Short: "Enable a disabled MCP server",
|
|
Long: `Re-enable a previously disabled MCP server.`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: MCPEnableHandler,
|
|
}
|
|
|
|
mcpCmd.AddCommand(addCmd, removeGlobalCmd, listGlobalCmd, disableCmd, enableCmd, pushCmd, pullCmd, listCmd, rmCmd, showCmd)
|
|
|
|
return mcpCmd
|
|
}
|