mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.aider*
|
||||
.env
|
||||
aidocs/
|
||||
@@ -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
|
||||
+110
@@ -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
|
||||
}
|
||||
+400
@@ -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
|
||||
}
|
||||
+565
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
Reference in New Issue
Block a user