From 94770db03a3f7635586bf8fe5cf6cbe0b3dd1e56 Mon Sep 17 00:00:00 2001 From: kriti-sc Date: Sun, 8 Jun 2025 17:39:49 -0700 Subject: [PATCH 1/3] add list servers command, to list servers in a config file --- cmd/mcptools/commands/servers.go | 51 ++++++++++++++++++++++++++++++++ cmd/mcptools/main.go | 1 + config/mcp_servers.json | 35 ++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 cmd/mcptools/commands/servers.go create mode 100644 config/mcp_servers.json diff --git a/cmd/mcptools/commands/servers.go b/cmd/mcptools/commands/servers.go new file mode 100644 index 0000000..a168c6b --- /dev/null +++ b/cmd/mcptools/commands/servers.go @@ -0,0 +1,51 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +// ServersConfig represents the root configuration structure +type ServersConfig struct { + MCPServers map[string]ServerConfig `json:"mcpServers"` +} + +// ServersCmd creates the servers command. +func ServersCmd() *cobra.Command { + return &cobra.Command{ + Use: "servers", + Short: "List the MCP servers installed", + Run: func(cmd *cobra.Command, args []string) { + // Read the config file + configPath := filepath.Join("/Users/kriti/Projects/mcptools/config/mcp_servers.json") + data, err := os.ReadFile(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) + os.Exit(1) + } + + // Parse the JSON + var config ServersConfig + if err := json.Unmarshal(data, &config); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", err) + os.Exit(1) + } + + // Print each server configuration + for name, server := range config.MCPServers { + // Print server name and command + fmt.Printf("%s: %s", name, server.Command) + + // Print arguments + for _, arg := range server.Args { + fmt.Printf(" %s", arg) + } + fmt.Println() + } + }, + } +} diff --git a/cmd/mcptools/main.go b/cmd/mcptools/main.go index 2d2c91a..288ae4f 100644 --- a/cmd/mcptools/main.go +++ b/cmd/mcptools/main.go @@ -41,6 +41,7 @@ func main() { commands.ConfigsCmd(), commands.NewCmd(), commands.GuardCmd(), + commands.ServersCmd(), ) if err := rootCmd.Execute(); err != nil { diff --git a/config/mcp_servers.json b/config/mcp_servers.json new file mode 100644 index 0000000..e34685c --- /dev/null +++ b/config/mcp_servers.json @@ -0,0 +1,35 @@ +{ + "mcpServers": { + "Atlan MCP": { + "command": "uv", + "args": [ + "run", + "/path/to/your/agent-toolkit/modelcontextprotocol/.venv/bin/atlan-mcp-server" + ], + "env": { + "ATLAN_API_KEY": "your_api_key", + "ATLAN_BASE_URL": "https://your-instance.atlan.com", + "ATLAN_AGENT_ID": "your_agent_id" + } + }, +"memory": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-memory"] + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/allowed/files" + ] + }, + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } + } + } +} From a5675deb6f922976c943f8687873e6ffef4f62cd Mon Sep 17 00:00:00 2001 From: kriti-sc Date: Sun, 8 Jun 2025 17:52:22 -0700 Subject: [PATCH 2/3] add standard location for servers config --- cmd/mcptools/commands/servers.go | 4 ++-- cmd/mcptools/main.go | 7 ++++++- config/mcp_servers.json | 35 -------------------------------- 3 files changed, 8 insertions(+), 38 deletions(-) delete mode 100644 config/mcp_servers.json diff --git a/cmd/mcptools/commands/servers.go b/cmd/mcptools/commands/servers.go index a168c6b..7c8b980 100644 --- a/cmd/mcptools/commands/servers.go +++ b/cmd/mcptools/commands/servers.go @@ -15,13 +15,13 @@ type ServersConfig struct { } // ServersCmd creates the servers command. -func ServersCmd() *cobra.Command { +func ServersCmd(configPath string) *cobra.Command { return &cobra.Command{ Use: "servers", Short: "List the MCP servers installed", Run: func(cmd *cobra.Command, args []string) { // Read the config file - configPath := filepath.Join("/Users/kriti/Projects/mcptools/config/mcp_servers.json") + configPath := filepath.Join(configPath) data, err := os.ReadFile(configPath) if err != nil { fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) diff --git a/cmd/mcptools/main.go b/cmd/mcptools/main.go index 288ae4f..6a5874f 100644 --- a/cmd/mcptools/main.go +++ b/cmd/mcptools/main.go @@ -4,7 +4,9 @@ Package main implements mcp functionality. package main import ( + "fmt" "os" + "path/filepath" "github.com/f/mcptools/cmd/mcptools/commands" "github.com/spf13/cobra" @@ -24,6 +26,9 @@ func init() { func main() { cobra.EnableCommandSorting = false + configPath := filepath.Join(os.Getenv("HOME"), "mcptools.config") + fmt.Fprintf(os.Stderr, "Loading server config from %s\n", configPath) + rootCmd := commands.RootCmd() rootCmd.AddCommand( commands.VersionCmd(), @@ -41,7 +46,7 @@ func main() { commands.ConfigsCmd(), commands.NewCmd(), commands.GuardCmd(), - commands.ServersCmd(), + commands.ServersCmd(configPath), ) if err := rootCmd.Execute(); err != nil { diff --git a/config/mcp_servers.json b/config/mcp_servers.json deleted file mode 100644 index e34685c..0000000 --- a/config/mcp_servers.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "mcpServers": { - "Atlan MCP": { - "command": "uv", - "args": [ - "run", - "/path/to/your/agent-toolkit/modelcontextprotocol/.venv/bin/atlan-mcp-server" - ], - "env": { - "ATLAN_API_KEY": "your_api_key", - "ATLAN_BASE_URL": "https://your-instance.atlan.com", - "ATLAN_AGENT_ID": "your_agent_id" - } - }, -"memory": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-memory"] - }, - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/path/to/allowed/files" - ] - }, - "github": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-github"], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "" - } - } - } -} From 68cf0224e3524efb42b72501757c7c5977f36127 Mon Sep 17 00:00:00 2001 From: kriti-sc Date: Fri, 13 Jun 2025 23:45:33 -0700 Subject: [PATCH 3/3] standardize mcp servers config path; add readme and tests --- README.md | 36 +++++++ cmd/mcptools/commands/servers.go | 9 +- cmd/mcptools/commands/servers_test.go | 131 ++++++++++++++++++++++++++ cmd/mcptools/main.go | 11 ++- cmd/mcptools/main_test.go | 130 +++++++++++++++++++++++++ 5 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 cmd/mcptools/commands/servers_test.go diff --git a/README.md b/README.md index fd00e03..9f5d844 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Available Commands: alias Manage MCP server aliases configs Manage MCP server configurations new Create a new MCP project component + servers List the MCP servers installed in file $HOME/mcptools.config help Help about any command completion Generate the autocompletion script for the specified shell @@ -213,6 +214,12 @@ mcp tools --format pretty npx -y @modelcontextprotocol/server-filesystem ~ MCP Tools includes several core commands for interacting with MCP servers: +#### List Installed Servers +Configure MCP Servers in $HOME/mcptools.config. See how to below in section `Installing MCP Servers` +```bash +mcp servers +``` + #### List Available Tools ```bash @@ -277,6 +284,35 @@ read_multiple_files(paths:str[]) This can be helpful for debugging or understanding what's happening on the server side when executing these commands. +### Configuring MCP Servers +You can provide configure of MCP Servers available in file $HOME/mcptools.config in the following format: +``` +{ + "mcpServers": { + "memory": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-memory"] + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/allowed/files" + ] + }, + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } + } + } +} +``` + + ### Interactive Shell The interactive shell mode allows you to run multiple MCP commands in a single session: diff --git a/cmd/mcptools/commands/servers.go b/cmd/mcptools/commands/servers.go index 7c8b980..eaf007e 100644 --- a/cmd/mcptools/commands/servers.go +++ b/cmd/mcptools/commands/servers.go @@ -15,11 +15,16 @@ type ServersConfig struct { } // ServersCmd creates the servers command. -func ServersCmd(configPath string) *cobra.Command { +func ServersCmd(configPath string, enableServers bool) *cobra.Command { return &cobra.Command{ Use: "servers", - Short: "List the MCP servers installed", + Short: "List the MCP servers installed in file $HOME/mcptools.config", Run: func(cmd *cobra.Command, args []string) { + if !enableServers { + fmt.Fprintf(os.Stderr, "Servers command is not enabled, please create a server config at %s\n", configPath) + return + } + // Read the config file configPath := filepath.Join(configPath) data, err := os.ReadFile(configPath) diff --git a/cmd/mcptools/commands/servers_test.go b/cmd/mcptools/commands/servers_test.go new file mode 100644 index 0000000..d9a7750 --- /dev/null +++ b/cmd/mcptools/commands/servers_test.go @@ -0,0 +1,131 @@ +package commands + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "testing" +) + +func TestServersCmd(t *testing.T) { + // Create a temporary directory for test files + tempDir, err := os.MkdirTemp("", "mcptools-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create test config file + testConfig := ServersConfig{ + MCPServers: map[string]ServerConfig{ + "test-server": { + Command: "test-command", + Args: []string{"arg1", "arg2"}, + }, + "another-server": { + Command: "another-command", + Args: []string{"arg3"}, + }, + }, + } + + configPath := filepath.Join(tempDir, "mcp_servers.json") + configData, err := json.Marshal(testConfig) + if err != nil { + t.Fatalf("Failed to marshal test config: %v", err) + } + + if err := os.WriteFile(configPath, configData, 0644); err != nil { + t.Fatalf("Failed to write test config: %v", err) + } + + tests := []struct { + name string + configPath string + enableServers bool + expectedOutput string + expectError bool + }{ + { + name: "successful server list", + configPath: configPath, + enableServers: true, + expectedOutput: `another-server: another-command arg3 +test-server: test-command arg1 arg2 +`, + expectError: false, + }, + { + name: "servers disabled", + configPath: configPath, + enableServers: false, + expectedOutput: `Servers command is not enabled, please create a server config at ` + configPath + "\n", + expectError: false, + }, + { + name: "non-existent config file", + configPath: filepath.Join(tempDir, "nonexistent.json"), + enableServers: true, + expectError: true, + }, + { + name: "invalid json config", + configPath: filepath.Join(tempDir, "invalid.json"), + enableServers: true, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create invalid JSON file for the invalid json test + if tt.name == "invalid json config" { + if err := os.WriteFile(tt.configPath, []byte("invalid json"), 0644); err != nil { + t.Fatalf("Failed to write invalid config: %v", err) + } + } + + // Create pipes for capturing output + oldStdout := os.Stdout + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stdout = w + os.Stderr = w + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + }() + + // Create a command with the test configuration + cmd := ServersCmd(tt.configPath, tt.enableServers) + + // Execute the command + err := cmd.Execute() + + // Close the write end of the pipe + w.Close() + + // Read the output + output, _ := io.ReadAll(r) + + // Check error conditions + if tt.expectError { + if err == nil { + t.Error("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // Check output + if got := string(output); got != tt.expectedOutput { + t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, got) + } + }) + } +} diff --git a/cmd/mcptools/main.go b/cmd/mcptools/main.go index 6a5874f..1819cb6 100644 --- a/cmd/mcptools/main.go +++ b/cmd/mcptools/main.go @@ -26,8 +26,15 @@ func init() { func main() { cobra.EnableCommandSorting = false + // check if mcptools.config exists configPath := filepath.Join(os.Getenv("HOME"), "mcptools.config") - fmt.Fprintf(os.Stderr, "Loading server config from %s\n", configPath) + enableServers := false + if _, err := os.Stat(configPath); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "No server config found at %s, servers command will not be available.\n", configPath) + } else { + fmt.Fprintf(os.Stderr, "Loading server config from %s\n", configPath) + enableServers = true + } rootCmd := commands.RootCmd() rootCmd.AddCommand( @@ -46,7 +53,7 @@ func main() { commands.ConfigsCmd(), commands.NewCmd(), commands.GuardCmd(), - commands.ServersCmd(configPath), + commands.ServersCmd(configPath, enableServers), ) if err := rootCmd.Execute(); err != nil { diff --git a/cmd/mcptools/main_test.go b/cmd/mcptools/main_test.go index 1be87c8..71ad8a4 100644 --- a/cmd/mcptools/main_test.go +++ b/cmd/mcptools/main_test.go @@ -537,3 +537,133 @@ func TestShellCommands(t *testing.T) { }) } } + +func TestMain(t *testing.T) { + // Create a temporary directory for test files + tempDir, err := os.MkdirTemp("", "mcptools-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create test config file + testConfig := commands.ServersConfig{ + MCPServers: map[string]commands.ServerConfig{ + "test-server": { + Command: "test-command", + Args: []string{"arg1", "arg2"}, + }, + }, + } + + configPath := filepath.Join(tempDir, "mcp_servers.json") + configData, err := json.Marshal(testConfig) + if err != nil { + t.Fatalf("Failed to marshal test config: %v", err) + } + + if err := os.WriteFile(configPath, configData, 0644); err != nil { + t.Fatalf("Failed to write test config: %v", err) + } + + tests := []struct { + name string + configPath string + args []string + expectedOutput string + expectError bool + }{ + { + name: "servers command with valid config", + configPath: configPath, + args: []string{"servers"}, + expectedOutput: `test-server: test-command arg1 arg2 +`, + expectError: false, + }, + { + name: "servers command with non-existent config", + configPath: filepath.Join(tempDir, "nonexistent.json"), + args: []string{"servers"}, + expectedOutput: `Servers command is not enabled, please create a server config at ` + filepath.Join(tempDir, "nonexistent.json") + "\n", + expectError: false, + }, + { + name: "help command shows servers", + configPath: configPath, + args: []string{"--help"}, + expectedOutput: "servers", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up test environment + os.Setenv("HOME", tempDir) + defer os.Unsetenv("HOME") + + // Create pipes for capturing output + oldStdout := os.Stdout + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stdout = w + os.Stderr = w + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + }() + + // Create and execute root command + rootCmd := commands.RootCmd() + rootCmd.AddCommand( + commands.VersionCmd(), + commands.ToolsCmd(), + commands.ResourcesCmd(), + commands.PromptsCmd(), + commands.CallCmd(), + commands.GetPromptCmd(), + commands.ReadResourceCmd(), + commands.ShellCmd(), + commands.WebCmd(), + commands.MockCmd(), + commands.ProxyCmd(), + commands.AliasCmd(), + commands.ConfigsCmd(), + commands.NewCmd(), + commands.GuardCmd(), + commands.ServersCmd(tt.configPath, true), + ) + + // Set the command arguments + rootCmd.SetArgs(tt.args) + + // Execute the command + err := rootCmd.Execute() + + // Close the write end of the pipe + w.Close() + + // Read the output + output, _ := io.ReadAll(r) + + // Check error conditions + if tt.expectError { + if err == nil { + t.Error("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // Check output + if got := string(output); !strings.Contains(got, tt.expectedOutput) { + t.Errorf("Expected output to contain:\n%s\nGot:\n%s", tt.expectedOutput, got) + } + }) + } +}