From d80a14f660acf8ab5a7d5dbc4b68c87696eb9871 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 8 Dec 2024 19:27:53 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 129 ++++++++++++ cmd/mcp.go | 110 ++++++++++ cmd/ollama.go | 400 +++++++++++++++++++++++++++++++++++ cmd/root.go | 565 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 58 ++++++ go.sum | 127 ++++++++++++ main.go | 7 + 8 files changed, 1399 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/mcp.go create mode 100644 cmd/ollama.go create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..69f83157 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.aider* +.env +aidocs/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..92afe6b3 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# MCPHost + +A CLI host application that enables Large Language Models (LLMs) to interact with external tools through the Model Context Protocol (MCP). Currently supports both Claude 3.5 Sonnet and Ollama models. + +## Overview + +MCPHost acts as a host in the MCP client-server architecture, where: +- **Hosts** (like MCPHost) are LLM applications that manage connections and interactions +- **Clients** maintain 1:1 connections with MCP servers +- **Servers** provide context, tools, and capabilities to the LLMs + +This architecture allows language models to: +- Access external tools and data sources +- Maintain consistent context across interactions +- Execute commands and retrieve information safely + +## Features + +- Interactive conversations with either Claude 3.5 Sonnet or Ollama models +- Support for multiple concurrent MCP servers +- Dynamic tool discovery and integration +- Streaming responses from both Claude and Ollama +- Tool calling capabilities for both model types +- Configurable MCP server locations and arguments +- Consistent command interface across model types + +## Installation + +```bash +go install github.com/mark3labs/mcphost@latest +``` + +## Configuration + +1. For Claude access, set your Anthropic API key as an environment variable: +```bash +export ANTHROPIC_API_KEY='your-api-key' +``` + +2. For Ollama access, ensure you have Ollama installed and running locally with your desired models. + +3. Create an MCP configuration file at `~/mcp.json` (or specify location with `--config`): +```json +{ + "mcpServers": { + "sqlite": { + "command": "uvx", + "args": [ + "mcp-server-sqlite", + "--db-path", + "/tmp/foo.db" + ] + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/tmp" + ] + } + } +} +``` + +Each MCP server entry requires: +- `command`: The command to run (e.g., `uvx`, `npx`) +- `args`: Array of arguments for the command: + - For SQLite server: `mcp-server-sqlite` with database path + - For filesystem server: `@modelcontextprotocol/server-filesystem` with directory path + +## Usage + +### Using Claude 3.5 Sonnet +Run the tool with default config location (`~/mcp.json`): +```bash +mcphost +``` + +### Using Ollama +Run with a specific Ollama model: +```bash +mcphost ollama --model mistral +``` + +### Using a Custom Config File +```bash +mcphost --config /path/to/config.json +``` + +## Available Commands + +While chatting, you can use these commands: +- `/help`: Show available commands +- `/tools`: List all available tools +- `/servers`: List configured MCP servers +- `/quit`: Exit the application +- `Ctrl+C`: Exit at any time + +## Requirements + +- Go 1.18 or later +- For Claude: An Anthropic API key +- For Ollama: Local Ollama installation with desired models +- One or more MCP-compatible tool servers + +## MCP Server Compatibility + +MCPHost can work with any MCP-compliant server. For examples and reference implementations, see the [MCP Servers Repository](https://github.com/modelcontextprotocol/servers). + +## Contributing + +Contributions are welcome! Feel free to: +- Submit bug reports or feature requests through issues +- Create pull requests for improvements +- Share your custom MCP servers +- Improve documentation + +Please ensure your contributions follow good coding practices and include appropriate tests. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Thanks to the Anthropic team for Claude and the MCP specification +- Thanks to the Ollama team for their local LLM runtime +- Thanks to all contributors who have helped improve this tool diff --git a/cmd/mcp.go b/cmd/mcp.go new file mode 100644 index 00000000..0f15a741 --- /dev/null +++ b/cmd/mcp.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/charmbracelet/log" + + "github.com/anthropics/anthropic-sdk-go" + mcpclient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +type MCPConfig struct { + MCPServers map[string]struct { + Command string `json:"command"` + Args []string `json:"args"` + } `json:"mcpServers"` +} + +func mcpToolsToAnthropicTools(serverName string, mcpTools []mcp.Tool) []anthropic.ToolParam { + anthropicTools := make([]anthropic.ToolParam, len(mcpTools)) + + for i, tool := range mcpTools { + namespacedName := fmt.Sprintf("%s__%s", serverName, tool.Name) + + schemaMap := map[string]interface{}{ + "type": tool.InputSchema.Type, + "properties": tool.InputSchema.Properties, + } + if len(tool.InputSchema.Required) > 0 { + schemaMap["required"] = tool.InputSchema.Required + } + + anthropicTools[i] = anthropic.ToolParam{ + Name: anthropic.F(namespacedName), + Description: anthropic.F(tool.Description), + InputSchema: anthropic.Raw[interface{}](schemaMap), + } + } + + return anthropicTools +} + +func loadMCPConfig() (*MCPConfig, error) { + var configPath string + if configFile != "" { + configPath = configFile + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("error getting home directory: %w", err) + } + configPath = filepath.Join(homeDir, "mcp.json") + } + + configData, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("error reading config file %s: %w", configPath, err) + } + + var config MCPConfig + if err := json.Unmarshal(configData, &config); err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + + return &config, nil +} + +func createMCPClients(config *MCPConfig) (map[string]*mcpclient.StdioMCPClient, error) { + clients := make(map[string]*mcpclient.StdioMCPClient) + + for name, server := range config.MCPServers { + client, err := mcpclient.NewStdioMCPClient(server.Command, server.Args...) + if err != nil { + for _, c := range clients { + c.Close() + } + return nil, fmt.Errorf("failed to create MCP client for %s: %w", name, err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + log.Info("Initializing server...", "name", name) + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "mcphost", + Version: "0.1.0", + } + + _, err = client.Initialize(ctx, initRequest) + if err != nil { + client.Close() + for _, c := range clients { + c.Close() + } + return nil, fmt.Errorf("failed to initialize MCP client for %s: %w", name, err) + } + + clients[name] = client + } + + return clients, nil +} diff --git a/cmd/ollama.go b/cmd/ollama.go new file mode 100644 index 00000000..b6aa3f7b --- /dev/null +++ b/cmd/ollama.go @@ -0,0 +1,400 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/huh/spinner" + "github.com/charmbracelet/log" + mcpclient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" + api "github.com/ollama/ollama/api" + "github.com/spf13/cobra" +) + +// F is a helper function to get a pointer to a value +func F[T any](v T) *T { + return &v +} + +var ( + modelName string + ollamaCmd = &cobra.Command{ + Use: "ollama", + Short: "Chat using an Ollama model", + Long: `Use a local Ollama model for chat with MCP tool support`, + RunE: func(cmd *cobra.Command, args []string) error { + return runOllama() + }, + } +) + +func init() { + ollamaCmd.Flags(). + StringVar(&modelName, "model", "", "Ollama model to use (required)") + ollamaCmd.MarkFlagRequired("model") + rootCmd.AddCommand(ollamaCmd) +} + +func mcpToolsToOllamaTools(serverName string, mcpTools []mcp.Tool) []api.Tool { + ollamaTools := make([]api.Tool, len(mcpTools)) + + for i, tool := range mcpTools { + namespacedName := fmt.Sprintf("%s__%s", serverName, tool.Name) + + ollamaTools[i] = api.Tool{ + Type: "function", + Function: api.ToolFunction{ + Name: namespacedName, + Description: tool.Description, + Parameters: struct { + Type string `json:"type"` + Required []string `json:"required"` + Properties map[string]struct { + Type string `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` + } `json:"properties"` + }{ + Type: tool.InputSchema.Type, + Required: tool.InputSchema.Required, + Properties: make(map[string]struct { + Type string `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` + }), + }, + }, + } + + // Convert properties + for propName, prop := range tool.InputSchema.Properties { + propMap, ok := prop.(map[string]interface{}) + if !ok { + log.Error("Invalid property type", "property", propName) + continue + } + + propType, _ := propMap["type"].(string) + propDesc, _ := propMap["description"].(string) + propEnumRaw, hasEnum := propMap["enum"] + + var enumVals []string + if hasEnum { + if enumSlice, ok := propEnumRaw.([]interface{}); ok { + enumVals = make([]string, len(enumSlice)) + for i, v := range enumSlice { + if str, ok := v.(string); ok { + enumVals[i] = str + } + } + } + } + + ollamaTools[i].Function.Parameters.Properties[propName] = struct { + Type string `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` + }{ + Type: propType, + Description: propDesc, + Enum: enumVals, + } + } + } + + return ollamaTools +} + +func runOllama() error { + mcpConfig, err := loadMCPConfig() + if err != nil { + return fmt.Errorf("error loading MCP config: %v", err) + } + + mcpClients, err := createMCPClients(mcpConfig) + if err != nil { + return fmt.Errorf("error creating MCP clients: %v", err) + } + + defer func() { + log.Info("Shutting down MCP servers...") + for name, client := range mcpClients { + if err := client.Close(); err != nil { + log.Error("Failed to close server", "name", name, "error", err) + } else { + log.Info("Server closed", "name", name) + } + } + }() + + client, err := api.ClientFromEnvironment() + if err != nil { + return fmt.Errorf("error creating Ollama client: %v", err) + } + + var allTools []api.Tool + for serverName, mcpClient := range mcpClients { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + toolsResult, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{}) + cancel() + + if err != nil { + log.Error( + "Error fetching tools", + "server", + serverName, + "error", + err, + ) + continue + } + + serverTools := mcpToolsToOllamaTools(serverName, toolsResult.Tools) + allTools = append(allTools, serverTools...) + log.Info( + "Tools loaded", + "server", + serverName, + "count", + len(toolsResult.Tools), + ) + } + + if err := updateRenderer(); err != nil { + return fmt.Errorf("error initializing renderer: %v", err) + } + + // Initialize messages with system prompt + messages := []api.Message{ + { + Role: "system", + Content: `You are a helpful AI assistant with access to external tools. Respond directly to questions and requests. +Only use tools when specifically needed to accomplish a task. If you can answer without using tools, do so. +When you do need to use a tool, explain what you're doing first.`, + }, + } + + // Main interaction loop + for { + width := getTerminalWidth() + var prompt string + form := huh.NewForm( + huh.NewGroup( + huh.NewText(). + Key("prompt"). + Title("Enter your prompt (Type /help for commands, Ctrl+C to quit)"). + Value(&prompt), + ), + ).WithWidth(width) + + err := form.Run() + if err != nil { + if err.Error() == "user aborted" { + fmt.Println("\nGoodbye!") + return nil + } + return err + } + + prompt = form.GetString("prompt") + if prompt == "" { + continue + } + + // Handle slash commands + if strings.HasPrefix(prompt, "/") { + switch strings.ToLower(strings.TrimSpace(prompt)) { + case "/tools": + handleToolsCommand(mcpClients) + continue + case "/help": + handleHelpCommand() + continue + case "/servers": + handleServersCommand(mcpConfig) + continue + case "/quit": + fmt.Println("\nGoodbye!") + return nil + default: + fmt.Printf("%s\nType /help to see available commands\n\n", + errorStyle.Render("Unknown command: "+prompt)) + continue + } + } + + err = runOllamaPrompt(client, mcpClients, allTools, prompt, &messages) + if err != nil { + return err + } + } +} + +func runOllamaPrompt( + client *api.Client, + mcpClients map[string]*mcpclient.StdioMCPClient, + tools []api.Tool, + prompt string, + messages *[]api.Message, +) error { + if prompt != "" { + fmt.Printf("\n%s\n", promptStyle.Render("You: "+prompt)) + *messages = append(*messages, api.Message{ + Role: "user", + Content: prompt, + }) + } + + var err error + var responseContent string + var toolCalls []api.ToolCall + + action := func() { + err = client.Chat(context.Background(), &api.ChatRequest{ + Model: modelName, + Messages: *messages, + Tools: tools, + Stream: F(false), // Disable streaming + }, func(response api.ChatResponse) error { + if response.Done { + responseContent = response.Message.Content + toolCalls = response.Message.ToolCalls + if len(toolCalls) > 0 && responseContent == "" { + responseContent = "Using tools..." + } + *messages = append(*messages, response.Message) + } + return nil + }) + } + + _ = spinner.New().Title("Thinking...").Action(action).Run() + if err != nil { + return err + } + + // Print the response + if err := updateRenderer(); err != nil { + return fmt.Errorf("error updating renderer: %v", err) + } + + fmt.Print(responseStyle.Render("\nAssistant: ")) + rendered, err := renderer.Render(responseContent + "\n") + if err != nil { + log.Error("Failed to render response", "error", err) + fmt.Print(responseContent + "\n") + } else { + fmt.Print(rendered) + } + + // Handle tool calls if present + if len(toolCalls) > 0 { + for _, toolCall := range toolCalls { + log.Info( + "🔧 Using tool", + "name", + toolCall.Function.Name, + ) + + parts := strings.Split(toolCall.Function.Name, "__") + if len(parts) != 2 { + fmt.Printf( + "Error: Invalid tool name format: %s\n", + toolCall.Function.Name, + ) + continue + } + + serverName, toolName := parts[0], parts[1] + mcpClient, ok := mcpClients[serverName] + if !ok { + fmt.Printf("Error: Server not found: %s\n", serverName) + continue + } + + var toolResultPtr *mcp.CallToolResult + action := func() { + ctx, cancel := context.WithTimeout( + context.Background(), + 10*time.Second, + ) + defer cancel() + + toolResultPtr, err = mcpClient.CallTool( + ctx, + mcp.CallToolRequest{ + Params: struct { + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments,omitempty"` + }{ + Name: toolName, + Arguments: toolCall.Function.Arguments, + }, + }, + ) + } + _ = spinner.New(). + Title(fmt.Sprintf("Running tool %s...", toolName)). + Action(action). + Run() + + if err != nil { + errMsg := fmt.Sprintf( + "Error calling tool %s: %v", + toolName, + err, + ) + fmt.Printf("\n%s\n", errorStyle.Render(errMsg)) + + // Add error message directly to messages array as JSON string + *messages = append(*messages, api.Message{ + Role: "tool", + Content: fmt.Sprintf(`{"error": "%s"}`, errMsg), + }) + continue + } + + toolResult := *toolResultPtr + log.Debug("Tool result details", + "isError", toolResult.IsError, + "result", toolResult.Result, + "raw", fmt.Sprintf("%+v", toolResult)) + + // Check if there's an error in the tool result + if toolResult.IsError { + errMsg := fmt.Sprintf("Tool error: %v", toolResult.Result) + fmt.Printf("\n%s\n", errorStyle.Render(errMsg)) + *messages = append(*messages, api.Message{ + Role: "tool", + Content: fmt.Sprintf(`{"error": %q}`, errMsg), + }) + continue + } + + // Add the tool result directly to messages array as JSON string + resultJSON, err := json.Marshal(toolResult.Content) + if err != nil { + errMsg := fmt.Sprintf("Error marshaling tool result: %v", err) + fmt.Printf("\n%s\n", errorStyle.Render(errMsg)) + continue + } + + *messages = append(*messages, api.Message{ + Role: "tool", + Content: string(resultJSON), + }) + + } + + // Make another call to get Ollama's response to the tool results + return runOllamaPrompt(client, mcpClients, tools, "", messages) + } + + fmt.Println() // Add spacing + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..3672a44a --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,565 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/huh/spinner" + "github.com/charmbracelet/log" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + mcpclient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +var ( + renderer *glamour.TermRenderer + + // Tokyo Night theme colors + tokyoPurple = lipgloss.Color("99") // #9d7cd8 + tokyoCyan = lipgloss.Color("73") // #7dcfff + tokyoBlue = lipgloss.Color("111") // #7aa2f7 + tokyoGreen = lipgloss.Color("120") // #73daca + tokyoRed = lipgloss.Color("203") // #f7768e + tokyoOrange = lipgloss.Color("215") // #ff9e64 + tokyoFg = lipgloss.Color("189") // #c0caf5 + tokyoGray = lipgloss.Color("237") // #3b4261 + tokyoBg = lipgloss.Color("234") // #1a1b26 + + serverCommandStyle = lipgloss.NewStyle(). + Foreground(tokyoOrange). + Bold(true) + + serverArgumentsStyle = lipgloss.NewStyle(). + Foreground(tokyoFg) + + serverHeaderStyle = lipgloss.NewStyle(). + Foreground(tokyoCyan). + Bold(true) + + promptStyle = lipgloss.NewStyle(). + Foreground(tokyoBlue). + PaddingLeft(2) + + responseStyle = lipgloss.NewStyle(). + Foreground(tokyoFg). + PaddingLeft(2) + + errorStyle = lipgloss.NewStyle(). + Foreground(tokyoRed). + Bold(true) + + serverBox = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(tokyoPurple). + Padding(1). + MarginBottom(1). + AlignHorizontal(lipgloss.Left) // Force left alignment + + toolNameStyle = lipgloss.NewStyle(). + Foreground(tokyoCyan). + Bold(true) + + descriptionStyle = lipgloss.NewStyle(). + Foreground(tokyoFg). + PaddingBottom(1) + + configFile string +) + +var rootCmd = &cobra.Command{ + Use: "mcphost", + Short: "Chat with Claude 3.5 Sonnet or Ollama models", + Long: `MCPHost is a CLI tool that allows you to interact with Claude 3.5 Sonnet or Ollama models. +It supports various tools through MCP servers and provides streaming responses.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runMCPHost() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags(). + StringVar(&configFile, "config", "", "config file (default is $HOME/mcp.json)") +} + +func getTerminalWidth() int { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + return 80 // Fallback width + } + return width - 20 +} + +func updateRenderer() error { + width := getTerminalWidth() + var err error + renderer, err = glamour.NewTermRenderer( + glamour.WithAutoStyle(), + glamour.WithWordWrap(width), + ) + return err +} + +func handleHelpCommand() { + if err := updateRenderer(); err != nil { + fmt.Printf( + "\n%s\n", + errorStyle.Render(fmt.Sprintf("Error updating renderer: %v", err)), + ) + return + } + var markdown strings.Builder + + markdown.WriteString("# Available Commands\n\n") + markdown.WriteString("The following commands are available:\n\n") + markdown.WriteString("- **/help**: Show this help message\n") + markdown.WriteString("- **/tools**: List all available tools\n") + markdown.WriteString("- **/servers**: List configured MCP servers\n") + markdown.WriteString("- **/quit**: Exit the application\n") + markdown.WriteString("\nYou can also press Ctrl+C at any time to quit.\n") + markdown.WriteString("\n## Subcommands\n\n") + markdown.WriteString( + "- **ollama**: Use an Ollama model instead of Claude\n", + ) + markdown.WriteString(" Example: `mcphost ollama --model mistral`\n") + + rendered, err := renderer.Render(markdown.String()) + if err != nil { + fmt.Printf( + "\n%s\n", + errorStyle.Render(fmt.Sprintf("Error rendering help: %v", err)), + ) + return + } + + fmt.Print(rendered) +} + +func handleServersCommand(config *MCPConfig) { + if err := updateRenderer(); err != nil { + fmt.Printf( + "\n%s\n", + errorStyle.Render(fmt.Sprintf("Error updating renderer: %v", err)), + ) + return + } + + var markdown strings.Builder + action := func() { + if len(config.MCPServers) == 0 { + markdown.WriteString("No servers configured.\n") + } else { + for name, server := range config.MCPServers { + markdown.WriteString(fmt.Sprintf("# %s\n\n", name)) + markdown.WriteString("*Command*\n") + markdown.WriteString(fmt.Sprintf("`%s`\n\n", server.Command)) + + markdown.WriteString("*Arguments*\n") + if len(server.Args) > 0 { + markdown.WriteString(fmt.Sprintf("`%s`\n", strings.Join(server.Args, " "))) + } else { + markdown.WriteString("*None*\n") + } + } + } + } + + _ = spinner.New(). + Title("Loading server configuration..."). + Action(action). + Run() + rendered, err := renderer.Render(markdown.String()) + if err != nil { + fmt.Printf( + "\n%s\n", + errorStyle.Render(fmt.Sprintf("Error rendering servers: %v", err)), + ) + return + } + + // Calculate width with proper margins + termWidth := getTerminalWidth() + contentWidth := termWidth - 20 // Reserve space for margins + + // Create a box style with the calculated width + boxStyle := serverBox.Width(contentWidth) + + // Wrap the rendered markdown in the box + boxedContent := boxStyle.Render(rendered) + fmt.Print(boxedContent) +} + +func handleToolsCommand(mcpClients map[string]*mcpclient.StdioMCPClient) { + if err := updateRenderer(); err != nil { + fmt.Printf( + "\n%s\n", + errorStyle.Render(fmt.Sprintf("Error updating renderer: %v", err)), + ) + return + } + + // Calculate widths with proper margins + termWidth := getTerminalWidth() + contentWidth := termWidth - 20 // Reserve space for margins + + // Update styles with calculated widths + serverBoxStyle := serverBox.Width(contentWidth) + + type serverTools struct { + tools []mcp.Tool + err error + } + results := make(map[string]serverTools) + + action := func() { + for serverName, mcpClient := range mcpClients { + ctx, cancel := context.WithTimeout( + context.Background(), + 10*time.Second, + ) + defer cancel() // Move defer here to ensure it's called + + toolsResult, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{}) + if err != nil { + results[serverName] = serverTools{ + tools: nil, + err: err, + } + continue + } + + // Only access Tools if toolsResult is not nil + var tools []mcp.Tool + if toolsResult != nil { + tools = toolsResult.Tools + } + + results[serverName] = serverTools{ + tools: tools, + err: nil, + } + } + } + _ = spinner.New(). + Title("Fetching tools from all servers..."). + Action(action). + Run() + + // Now display all results + for serverName, result := range results { + if result.err != nil { + errMsg := errorStyle.Render( + fmt.Sprintf( + "Error fetching tools from %s: %v", + serverName, + result.err, + ), + ) + fmt.Printf("\n%s\n", serverBox.Render(errMsg)) + continue + } + + serverHeader := fmt.Sprintf("# %s\n", serverName) + renderedHeader, err := renderer.Render(serverHeader) + if err != nil { + errMsg := errorStyle.Render( + fmt.Sprintf("Error rendering server header: %v", err), + ) + fmt.Printf("\n%s\n", serverBox.Render(errMsg)) + continue + } + + var content strings.Builder + content.WriteString(renderedHeader) + + if len(result.tools) == 0 { + content.WriteString("\nNo tools available.\n") + } else { + for _, tool := range result.tools { + toolDisplay := fmt.Sprintf("%s\n%s", + toolNameStyle.Render("🔧 "+tool.Name), + descriptionStyle.Render(tool.Description), + ) + content.WriteString(toolDisplay + "\n") + } + } + + boxedContent := serverBoxStyle.Render(content.String()) + // Print directly without the centeredBox wrapper + fmt.Print(boxedContent) + } +} + +func runPrompt( + client *anthropic.Client, + mcpClients map[string]*mcpclient.StdioMCPClient, + tools []anthropic.ToolParam, + prompt string, + messages *[]anthropic.MessageParam, +) error { + // Display the user's prompt if it's not empty (i.e., not a tool response) + if prompt != "" { + fmt.Printf("\n%s\n", promptStyle.Render("You: "+prompt)) + *messages = append( + *messages, + anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)), + ) + } + + var messagePtr *anthropic.Message + var err error + action := func() { + messagePtr, err = client.Messages.New( + context.Background(), + anthropic.MessageNewParams{ + Model: anthropic.F( + anthropic.ModelClaude_3_5_Sonnet_20240620, + ), + MaxTokens: anthropic.F(int64(4096)), + Messages: anthropic.F(*messages), + Tools: anthropic.F(tools), + }, + ) + } + _ = spinner.New().Title("Thinking...").Action(action).Run() + + if err != nil { + return err + } + + message := *messagePtr // Dereference the pointer + fmt.Print(responseStyle.Render("\nClaude: ")) + + toolResults := []anthropic.ContentBlockParamUnion{} + + for _, block := range message.Content { + switch block := block.AsUnion().(type) { + case anthropic.TextBlock: + if err := updateRenderer(); err != nil { + return fmt.Errorf("error updating renderer: %v", err) + } + str, err := renderer.Render(block.Text + "\n") + if err != nil { + log.Error("Failed to render response", "error", err) + fmt.Print(block.Text + "\n") + continue + } + fmt.Print(str) + + case anthropic.ToolUseBlock: + log.Info("🔧 Using tool", "name", block.Name) + + parts := strings.Split(block.Name, "__") + if len(parts) != 2 { + fmt.Printf("Error: Invalid tool name format: %s\n", block.Name) + continue + } + + serverName, toolName := parts[0], parts[1] + mcpClient, ok := mcpClients[serverName] + if !ok { + fmt.Printf("Error: Server not found: %s\n", serverName) + continue + } + + var toolArgs map[string]interface{} + if err := json.Unmarshal(block.Input, &toolArgs); err != nil { + fmt.Printf("Error parsing tool arguments: %v\n", err) + continue + } + + var toolResultPtr *mcp.CallToolResult + action := func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + toolResultPtr, err = mcpClient.CallTool(ctx, mcp.CallToolRequest{ + Params: struct { + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments,omitempty"` + }{ + Name: toolName, + Arguments: toolArgs, + }, + }) + } + _ = spinner.New(). + Title(fmt.Sprintf("Running tool %s...", toolName)). + Action(action). + Run() + + if err != nil { + errMsg := fmt.Sprintf("Error calling tool %s: %v", toolName, err) + fmt.Printf("\n%s\n", errorStyle.Render(errMsg)) + + // Add error message as tool result + toolResults = append(toolResults, + anthropic.NewToolResultBlock(block.ID, errMsg, true)) + continue + } + + toolResult := *toolResultPtr // Dereference the pointer + resultJSON, err := json.Marshal(toolResult) + if err != nil { + fmt.Printf("Error marshaling tool result: %v\n", err) + continue + } + + toolResults = append(toolResults, + anthropic.NewToolResultBlock(block.ID, string(resultJSON), toolResult.IsError)) + } + } + + *messages = append(*messages, message.ToParam()) + + if len(toolResults) > 0 { + *messages = append(*messages, anthropic.NewUserMessage(toolResults...)) + // Make another call to get Claude's response to the tool results + return runPrompt(client, mcpClients, tools, "", messages) + } + + fmt.Println() // Add spacing + return nil +} + +func runMCPHost() error { + apiKey := os.Getenv("ANTHROPIC_API_KEY") + if apiKey == "" { + return fmt.Errorf("ANTHROPIC_API_KEY environment variable not set") + } + + mcpConfig, err := loadMCPConfig() + if err != nil { + return fmt.Errorf("error loading MCP config: %v", err) + } + + mcpClients, err := createMCPClients(mcpConfig) + if err != nil { + return fmt.Errorf("error creating MCP clients: %v", err) + } + + defer func() { + log.Info("Shutting down MCP servers...") + for name, client := range mcpClients { + if err := client.Close(); err != nil { + log.Error("Failed to close server", "name", name, "error", err) + } else { + log.Info("Server closed", "name", name) + } + } + }() + + for name := range mcpClients { + log.Info("Server connected", "name", name) + } + + client := anthropic.NewClient( + option.WithAPIKey(apiKey), + ) + + var allTools []anthropic.ToolParam + for serverName, mcpClient := range mcpClients { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + toolsResult, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{}) + cancel() + + if err != nil { + log.Error( + "Error fetching tools", + "server", + serverName, + "error", + err, + ) + continue + } + + serverTools := mcpToolsToAnthropicTools(serverName, toolsResult.Tools) + allTools = append(allTools, serverTools...) + log.Info( + "Tools loaded", + "server", + serverName, + "count", + len(toolsResult.Tools), + ) + } + + if err := updateRenderer(); err != nil { + return fmt.Errorf("error initializing renderer: %v", err) + } + + messages := make([]anthropic.MessageParam, 0) + + // Main interaction loop + for { + width := getTerminalWidth() + var prompt string + form := huh.NewForm( + huh.NewGroup( + huh.NewText(). + Key("prompt"). + Title("Enter your prompt (Type /help for commands, Ctrl+C to quit)"). + Value(&prompt), + ), + ).WithWidth(width) + + err := form.Run() + if err != nil { + // Check if it's a user abort (Ctrl+C) + if err.Error() == "user aborted" { + fmt.Println("\nGoodbye!") + return nil // Exit cleanly + } + return err // Return other errors normally + } + + prompt = form.GetString("prompt") + if prompt == "" { + continue + } + + // Handle slash commands + if strings.HasPrefix(prompt, "/") { + switch strings.ToLower(strings.TrimSpace(prompt)) { + case "/tools": + handleToolsCommand(mcpClients) + continue + case "/help": + handleHelpCommand() + continue + case "/servers": + handleServersCommand(mcpConfig) + continue + case "/quit": + fmt.Println("\nGoodbye!") + return nil + default: + fmt.Printf("%s\nType /help to see available commands\n\n", + errorStyle.Render("Unknown command: "+prompt)) + continue + } + } + + err = runPrompt(client, mcpClients, allTools, prompt, &messages) + if err != nil { + return err + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..5f6036e0 --- /dev/null +++ b/go.mod @@ -0,0 +1,58 @@ +module github.com/mark3labs/mcphost + +go 1.23.3 + +require ( + github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.5 + github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/huh/spinner v0.0.0-20241127125741-aad810dfbce6 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/charmbracelet/log v0.4.0 + github.com/mark3labs/mcp-go v0.5.3 + github.com/ollama/ollama v0.5.1 + github.com/spf13/cobra v1.8.1 + golang.org/x/term v0.22.0 +) + +require ( + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect + golang.org/x/net v0.27.0 // indirect +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.2.4 // indirect + github.com/charmbracelet/glamour v0.8.0 + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..dfd5c927 --- /dev/null +++ b/go.sum @@ -0,0 +1,127 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.5 h1:Ew8EGOH+FUI5fsJmpM03jkQFpXkxY82fGrXE/3aaq9U= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.5/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= +github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= +github.com/charmbracelet/huh/spinner v0.0.0-20241127125741-aad810dfbce6 h1:btKBXcuvUcXRT0VVk850BOpov6wjCLAPoJuaPm+sCKU= +github.com/charmbracelet/huh/spinner v0.0.0-20241127125741-aad810dfbce6/go.mod h1:3/xTBdgqRzAb+eUKRAGi9ix/K6QxsS0nGtd4zp+/tJs= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mark3labs/mcp-go v0.5.3 h1:RGLO3vqB2t2rXFidwIm7vFvtJdcSAXVWklTVxcbNWYs= +github.com/mark3labs/mcp-go v0.5.3/go.mod h1:ePkDSyplFbA306xRgyp587+q/vpdgxuswwjZqTQ+I8Q= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/ollama/ollama v0.5.1 h1:Ug4y/5UZZoTgetMklZslAlEdaCnYEX9qZJ/aTsM4+xc= +github.com/ollama/ollama v0.5.1/go.mod h1:wrgnDTdogU9yeFOj/Jc8BpRBJrWu+Ox4eGyHxqiaQDc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 00000000..b001becd --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/mark3labs/mcphost/cmd" + +func main() { + cmd.Execute() +}