From f065cd408f4326c526358eec81cd630a9e1e41b7 Mon Sep 17 00:00:00 2001 From: ayakut16 Date: Fri, 4 Apr 2025 12:18:22 +0100 Subject: [PATCH 1/8] refactor: move commands in their own files --- cmd/mcptools/commands/alias.go | 147 +++ cmd/mcptools/commands/call.go | 120 +++ cmd/mcptools/commands/get_prompt.go | 88 ++ cmd/mcptools/commands/mock.go | 126 +++ cmd/mcptools/{ => commands}/new.go | 12 +- cmd/mcptools/commands/prompts.go | 39 + cmd/mcptools/commands/proxy.go | 234 +++++ cmd/mcptools/commands/read_resource.go | 89 ++ cmd/mcptools/commands/resources.go | 39 + cmd/mcptools/commands/root.go | 49 + cmd/mcptools/commands/shell.go | 347 +++++++ cmd/mcptools/commands/tools.go | 39 + cmd/mcptools/commands/utils.go | 85 ++ cmd/mcptools/commands/version.go | 21 + cmd/mcptools/commands/version_test.go | 63 ++ cmd/mcptools/main.go | 1308 +----------------------- cmd/mcptools/main_test.go | 9 +- 17 files changed, 1512 insertions(+), 1303 deletions(-) create mode 100644 cmd/mcptools/commands/alias.go create mode 100644 cmd/mcptools/commands/call.go create mode 100644 cmd/mcptools/commands/get_prompt.go create mode 100644 cmd/mcptools/commands/mock.go rename cmd/mcptools/{ => commands}/new.go (97%) create mode 100644 cmd/mcptools/commands/prompts.go create mode 100644 cmd/mcptools/commands/proxy.go create mode 100644 cmd/mcptools/commands/read_resource.go create mode 100644 cmd/mcptools/commands/resources.go create mode 100644 cmd/mcptools/commands/root.go create mode 100644 cmd/mcptools/commands/shell.go create mode 100644 cmd/mcptools/commands/tools.go create mode 100644 cmd/mcptools/commands/utils.go create mode 100644 cmd/mcptools/commands/version.go create mode 100644 cmd/mcptools/commands/version_test.go diff --git a/cmd/mcptools/commands/alias.go b/cmd/mcptools/commands/alias.go new file mode 100644 index 0000000..837428f --- /dev/null +++ b/cmd/mcptools/commands/alias.go @@ -0,0 +1,147 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/f/mcptools/pkg/alias" + "github.com/spf13/cobra" +) + +// AliasCmd creates the alias command. +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 ", + Short: "Remove an MCP server alias", + Long: `Remove a registered alias for an MCP server command. + +Example: + mcp alias remove myfs`, + Args: cobra.ExactArgs(1), + 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] + + 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' does not exist", 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 + }, + } +} diff --git a/cmd/mcptools/commands/call.go b/cmd/mcptools/commands/call.go new file mode 100644 index 0000000..a29d9c0 --- /dev/null +++ b/cmd/mcptools/commands/call.go @@ -0,0 +1,120 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +// CallCmd creates the call command. +func CallCmd() *cobra.Command { + return &cobra.Command{ + Use: "call entity [command args...]", + Short: "Call a tool, resource, or prompt on the MCP server", + DisableFlagParsing: true, + SilenceUsage: true, + Run: func(thisCmd *cobra.Command, args []string) { + if len(args) == 1 && (args[0] == FlagHelp || args[0] == FlagHelpShort) { + _ = thisCmd.Help() + return + } + + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "Error: entity name is required") + fmt.Fprintln( + os.Stderr, + "Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~", + ) + os.Exit(1) + } + + cmdArgs := args + parsedArgs := []string{} + entityName := "" + + i := 0 + entityExtracted := false + + for i < len(cmdArgs) { + switch { + case (cmdArgs[i] == FlagFormat || cmdArgs[i] == FlagFormatShort) && i+1 < len(cmdArgs): + FormatOption = cmdArgs[i+1] + i += 2 + case (cmdArgs[i] == FlagParams || cmdArgs[i] == FlagParamsShort) && i+1 < len(cmdArgs): + ParamsString = cmdArgs[i+1] + i += 2 + case !entityExtracted: + entityName = cmdArgs[i] + entityExtracted = true + i++ + default: + parsedArgs = append(parsedArgs, cmdArgs[i]) + i++ + } + } + + if entityName == "" { + fmt.Fprintln(os.Stderr, "Error: entity name is required") + fmt.Fprintln( + os.Stderr, + "Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~", + ) + os.Exit(1) + } + + entityType := EntityTypeTool + + parts := strings.SplitN(entityName, ":", 2) + if len(parts) == 2 { + entityType = parts[0] + entityName = parts[1] + } + + if len(parsedArgs) == 0 { + fmt.Fprintln(os.Stderr, "Error: command to execute is required when using stdio transport") + fmt.Fprintln( + os.Stderr, + "Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~", + ) + os.Exit(1) + } + + var params map[string]any + if ParamsString != "" { + if jsonErr := json.Unmarshal([]byte(ParamsString), ¶ms); jsonErr != nil { + fmt.Fprintf(os.Stderr, "Error: invalid JSON for params: %v\n", jsonErr) + os.Exit(1) + } + } + + mcpClient, clientErr := CreateClientFunc(parsedArgs) + if clientErr != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", clientErr) + os.Exit(1) + } + + var resp map[string]any + var execErr error + + switch entityType { + case EntityTypeTool: + resp, execErr = mcpClient.CallTool(entityName, params) + case EntityTypeRes: + resp, execErr = mcpClient.ReadResource(entityName) + case EntityTypePrompt: + resp, execErr = mcpClient.GetPrompt(entityName) + default: + fmt.Fprintf(os.Stderr, "Error: unsupported entity type: %s\n", entityType) + os.Exit(1) + } + + if formatErr := FormatAndPrintResponse(resp, execErr); formatErr != nil { + fmt.Fprintf(os.Stderr, "%v\n", formatErr) + os.Exit(1) + } + }, + } +} diff --git a/cmd/mcptools/commands/get_prompt.go b/cmd/mcptools/commands/get_prompt.go new file mode 100644 index 0000000..db3040d --- /dev/null +++ b/cmd/mcptools/commands/get_prompt.go @@ -0,0 +1,88 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// GetPromptCmd creates the get-prompt command. +func GetPromptCmd() *cobra.Command { + return &cobra.Command{ + Use: "get-prompt prompt [command args...]", + Short: "Get a prompt on the MCP server", + DisableFlagParsing: true, + SilenceUsage: true, + Run: func(thisCmd *cobra.Command, args []string) { + if len(args) == 1 && (args[0] == FlagHelp || args[0] == FlagHelpShort) { + _ = thisCmd.Help() + return + } + + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "Error: prompt name is required") + fmt.Fprintln( + os.Stderr, + "Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~", + ) + os.Exit(1) + } + + cmdArgs := args + parsedArgs := []string{} + promptName := "" + + i := 0 + promptExtracted := false + + for i < len(cmdArgs) { + switch { + case (cmdArgs[i] == FlagFormat || cmdArgs[i] == FlagFormatShort) && i+1 < len(cmdArgs): + FormatOption = cmdArgs[i+1] + i += 2 + case (cmdArgs[i] == FlagParams || cmdArgs[i] == FlagParamsShort) && i+1 < len(cmdArgs): + ParamsString = cmdArgs[i+1] + i += 2 + case !promptExtracted: + promptName = cmdArgs[i] + promptExtracted = true + i++ + default: + parsedArgs = append(parsedArgs, cmdArgs[i]) + i++ + } + } + + if promptName == "" { + fmt.Fprintln(os.Stderr, "Error: prompt name is required") + fmt.Fprintln( + os.Stderr, + "Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~", + ) + os.Exit(1) + } + + var params map[string]any + if ParamsString != "" { + if jsonErr := json.Unmarshal([]byte(ParamsString), ¶ms); jsonErr != nil { + fmt.Fprintf(os.Stderr, "Error: invalid JSON for params: %v\n", jsonErr) + os.Exit(1) + } + } + + mcpClient, clientErr := CreateClientFunc(parsedArgs) + if clientErr != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", clientErr) + os.Exit(1) + } + + resp, execErr := mcpClient.GetPrompt(promptName) + if formatErr := FormatAndPrintResponse(resp, execErr); formatErr != nil { + fmt.Fprintf(os.Stderr, "%v\n", formatErr) + os.Exit(1) + } + }, + } +} diff --git a/cmd/mcptools/commands/mock.go b/cmd/mcptools/commands/mock.go new file mode 100644 index 0000000..3f03700 --- /dev/null +++ b/cmd/mcptools/commands/mock.go @@ -0,0 +1,126 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/f/mcptools/pkg/mock" + "github.com/spf13/cobra" +) + +// MockCmd creates the mock command. +func MockCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "mock [type] [name] [description] [content]...", + Short: "Create a mock MCP server with tools, prompts, and resources", + Long: `Create a mock MCP server with tools, prompts, and resources. +This is useful for testing MCP clients without implementing a full server. + +The mock server implements the MCP protocol with: +- Full initialization handshake (initialize method) +- Support for notifications/initialized notification +- Tool listing with standardized schema format +- Tool calling with simple responses +- Resource listing and reading with proper format +- Prompt listing and retrieving with proper format +- Standard error codes (-32601 for method not found) +- Detailed request/response logging to ~/.mcpt/logs/mock.log + +Available types: +- tool +- prompt