Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Usage:
mcp [command]

Available Commands:
alias Manage MCP server aliases
call Call a tool, resource, or prompt on the MCP server
help Help about any command
mock Create a mock MCP server with tools, prompts, and resources
Expand Down Expand Up @@ -270,6 +271,27 @@ Special Commands:
/q, /quit, exit Exit the shell
```

## Server Aliases

MCP Tools allows you to save and reuse server commands with friendly aliases:

```bash
# Add a new server alias
mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/

# List all registered server aliases
mcp alias list

# Remove a server alias
mcp alias remove myfs

# Use an alias with any MCP command
mcp tools myfs
mcp call read_file --params '{"path": "README.md"}' myfs
```

Server aliases are stored in `$HOME/.mcpt/aliases.json` and provide a convenient way to work with commonly used MCP servers without typing long commands repeatedly.

## Server Modes

MCP Tools can operate as both a client and a server, with two server modes available:
Expand Down
148 changes: 147 additions & 1 deletion cmd/mcptools/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"path/filepath"
"strings"

"github.com/f/mcptools/pkg/alias"
"github.com/f/mcptools/pkg/client"
"github.com/f/mcptools/pkg/jsonutils"
"github.com/f/mcptools/pkg/mock"
Expand Down Expand Up @@ -56,7 +57,23 @@ var createClientFunc = func(args []string) (*client.Client, error) {
return nil, errCommandRequired
}

if len(args) == 1 && (strings.HasPrefix(args[0], "http://") || strings.HasPrefix(args[0], "https://")) {
isHTTP := func(str string) bool {
return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://")
}

// Check if the first argument is an alias
if len(args) == 1 {
server, found := alias.GetServerCommand(args[0])
if found {
if isHTTP(server) {
return client.NewHTTP(server), nil
}
cmdParts := client.ParseCommandString(server)
return client.NewStdio(cmdParts), nil
}
}

if len(args) == 1 && isHTTP(args[0]) {
return client.NewHTTP(args[0]), nil
}

Expand All @@ -78,6 +95,7 @@ func main() {
newShellCmd(),
newMockCmd(),
proxyCmd(),
aliasCmd(),
)

if err := rootCmd.Execute(); err != nil {
Expand Down Expand Up @@ -1162,3 +1180,131 @@ Example:

return cmd
}

func aliasCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "alias",
Short: "Manage MCP server aliases",
Long: `Manage aliases for MCP servers.

This command allows you to register MCP server commands with a friendly name and
reuse them later.

Aliases are stored in $HOME/.mcpt/aliases.json.

Examples:
# Add a new server alias
mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/

# List all registered server aliases
mcp alias list

# Remove a server alias
mcp alias remove myfs

# Use an alias with any MCP command
mcp tools myfs`,
}

cmd.AddCommand(aliasAddCmd())
cmd.AddCommand(aliasListCmd())
cmd.AddCommand(aliasRemoveCmd())

return cmd
}

func aliasAddCmd() *cobra.Command {
addCmd := &cobra.Command{
Use: "add [alias] [command args...]",
Short: "Add a new MCP server alias",
DisableFlagParsing: true,
Long: `Add a new alias for an MCP server command.

The alias will be registered and can be used in place of the server command.

Example:
mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/`,
Args: cobra.MinimumNArgs(2),
RunE: func(thisCmd *cobra.Command, args []string) error {
if len(args) == 1 && (args[0] == flagHelp || args[0] == flagHelpShort) {
_ = thisCmd.Help()
return nil
}

aliasName := args[0]
serverCommand := strings.Join(args[1:], " ")

aliases, err := alias.Load()
if err != nil {
return fmt.Errorf("error loading aliases: %w", err)
}

aliases[aliasName] = alias.ServerAlias{
Command: serverCommand,
}

if saveErr := alias.Save(aliases); saveErr != nil {
return fmt.Errorf("error saving aliases: %w", saveErr)
}

fmt.Printf("Alias '%s' registered for command: %s\n", aliasName, serverCommand)
return nil
},
}
return addCmd
}

func aliasListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all registered MCP server aliases",
RunE: func(_ *cobra.Command, _ []string) error {
// Load existing aliases
aliases, err := alias.Load()
if err != nil {
return fmt.Errorf("error loading aliases: %w", err)
}

if len(aliases) == 0 {
fmt.Println("No aliases registered.")
return nil
}

fmt.Println("Registered MCP server aliases:")
for name, a := range aliases {
fmt.Printf(" %s: %s\n", name, a.Command)
}

return nil
},
}
}

func aliasRemoveCmd() *cobra.Command {
return &cobra.Command{
Use: "remove [alias]",
Short: "Remove an MCP server alias",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
aliasName := args[0]

aliases, err := alias.Load()
if err != nil {
return fmt.Errorf("error loading aliases: %w", err)
}

if _, exists := aliases[aliasName]; !exists {
return fmt.Errorf("alias '%s' not found", aliasName)
}

delete(aliases, aliasName)

if saveErr := alias.Save(aliases); saveErr != nil {
return fmt.Errorf("error saving aliases: %w", saveErr)
}

fmt.Printf("Alias '%s' removed.\n", aliasName)
return nil
},
}
}
16 changes: 9 additions & 7 deletions cmd/mcptools/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"testing"

"github.com/f/mcptools/pkg/client"
"github.com/f/mcptools/pkg/transport"
)

Expand Down Expand Up @@ -323,7 +322,9 @@ func TestExecuteShell(t *testing.T) {
func TestProxyToolRegistration(t *testing.T) {
// Create a temporary directory for test files
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
if err := os.Setenv("HOME", tmpDir); err != nil {
t.Fatalf("Failed to set HOME environment variable: %v", err)
}

// Test cases
testCases := []struct {
Expand Down Expand Up @@ -397,7 +398,9 @@ func TestProxyToolRegistration(t *testing.T) {
func TestProxyToolUnregistration(t *testing.T) {
// Create a temporary directory for test files
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
if err := os.Setenv("HOME", tmpDir); err != nil {
t.Fatalf("Failed to set HOME environment variable: %v", err)
}

// First register a tool
cmd := proxyToolCmd()
Expand All @@ -413,7 +416,9 @@ func TestProxyToolUnregistration(t *testing.T) {
}

// Now try to unregister it
cmd.Flags().Set("unregister", "true")
if setErr := cmd.Flags().Set("unregister", "true"); setErr != nil {
t.Fatalf("Failed to set unregister flag: %v", setErr)
}
err = cmd.RunE(cmd, []string{"test_tool"})
if err != nil {
t.Errorf("Error unregistering tool: %v", err)
Expand All @@ -430,9 +435,6 @@ func TestProxyToolUnregistration(t *testing.T) {
}
}

// testCreateClient is a test-specific version of createClient that always returns a client with our mock transport
var testCreateClient func(args []string) (*client.Client, error)

func TestShellCommands(t *testing.T) {
// Create a mock server for testing
mockServer := NewMockTransport()
Expand Down
100 changes: 100 additions & 0 deletions pkg/alias/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Package alias implements server alias functionality for MCP.
*/
package alias

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)

// ServerAlias represents a single server command alias.
type ServerAlias struct {
Command string `json:"command"`
}

// Aliases stores command aliases for MCP servers.
type Aliases map[string]ServerAlias

// GetConfigPath returns the path to the aliases configuration file.
func GetConfigPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}

configDir := filepath.Join(homeDir, ".mcpt")
mkdirErr := os.MkdirAll(configDir, 0o750)
if mkdirErr != nil {
return "", fmt.Errorf("failed to create config directory: %w", mkdirErr)
}

return filepath.Join(configDir, "aliases.json"), nil
}

// Load loads server aliases from the configuration file.
func Load() (Aliases, error) {
configPath, err := GetConfigPath()
if err != nil {
return nil, err
}

aliases := make(Aliases)

var statErr error
if _, statErr = os.Stat(configPath); os.IsNotExist(statErr) {
return aliases, nil
}

configFile, err := os.ReadFile(configPath) // #nosec G304 - configPath is generated internally by GetConfigPath
if err != nil {
return nil, fmt.Errorf("failed to read alias config file: %w", err)
}

if len(configFile) == 0 {
return aliases, nil
}

if unmarshalErr := json.Unmarshal(configFile, &aliases); unmarshalErr != nil {
return nil, fmt.Errorf("failed to parse alias config file: %w", unmarshalErr)
}

return aliases, nil
}

// Save saves server aliases to the configuration file.
func Save(aliases Aliases) error {
configPath, err := GetConfigPath()
if err != nil {
return err
}

configJSON, err := json.MarshalIndent(aliases, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal alias config: %w", err)
}

writeErr := os.WriteFile(configPath, configJSON, 0o600) // #nosec G304 - configPath is generated internally by GetConfigPath
if writeErr != nil {
return fmt.Errorf("failed to write alias config file: %w", writeErr)
}

return nil
}

// GetServerCommand retrieves the server command for a given alias.
func GetServerCommand(aliasName string) (string, bool) {
aliases, err := Load()
if err != nil {
return "", false
}

alias, exists := aliases[aliasName]
if !exists {
return "", false
}

return alias.Command, true
}
Loading