Merge branch 'main' of github.com:mark3labs/mcphost

This commit is contained in:
Ed Zynda
2026-02-25 16:24:33 +03:00
15 changed files with 34137 additions and 14595 deletions
+33 -4
View File
@@ -38,6 +38,7 @@ var (
streamFlag bool // Enable streaming output
compactMode bool // Enable compact output mode
scriptMCPConfig *config.Config // Used to override config in script mode
approveToolRun bool
// Session management
saveSessionPath string
@@ -302,6 +303,8 @@ func init() {
BoolVar(&compactMode, "compact", false, "enable compact output mode without fancy styling")
rootCmd.PersistentFlags().
BoolVar(&noHooks, "no-hooks", false, "disable all hooks execution")
rootCmd.PersistentFlags().
BoolVar(&approveToolRun, "approve-tool-run", false, "enable requiring user approval for every tool call")
// Session management flags
rootCmd.PersistentFlags().
@@ -347,6 +350,7 @@ func init() {
viper.BindPFlag("num-gpu-layers", rootCmd.PersistentFlags().Lookup("num-gpu-layers"))
viper.BindPFlag("main-gpu", rootCmd.PersistentFlags().Lookup("main-gpu"))
viper.BindPFlag("tls-skip-verify", rootCmd.PersistentFlags().Lookup("tls-skip-verify"))
viper.BindPFlag("approve-tool-run", rootCmd.PersistentFlags().Lookup("approve-tool-run"))
// Defaults are already set in flag definitions, no need to duplicate in viper
@@ -445,7 +449,8 @@ func runNormalMode(ctx context.Context) error {
debugLogger = bufferedLogger
}
mcpAgent, err := agent.CreateAgent(ctx, &agent.AgentCreationOptions{ModelConfig: modelConfig,
mcpAgent, err := agent.CreateAgent(ctx, &agent.AgentCreationOptions{
ModelConfig: modelConfig,
MCPConfig: mcpConfig,
SystemPrompt: systemPrompt,
MaxSteps: viper.GetInt("max-steps"),
@@ -743,7 +748,8 @@ func runNormalMode(ctx context.Context) error {
return fmt.Errorf("--quiet flag can only be used with --prompt/-p")
}
return runInteractiveMode(ctx, mcpAgent, cli, serverNames, toolNames, modelName, messages, sessionManager, hookExecutor)
approveToolRun := viper.GetBool("approve-tool-run")
return runInteractiveMode(ctx, mcpAgent, cli, serverNames, toolNames, modelName, messages, sessionManager, hookExecutor, approveToolRun)
}
// AgenticLoopConfig configures the behavior of the unified agentic loop.
@@ -754,6 +760,7 @@ type AgenticLoopConfig struct {
IsInteractive bool // true for interactive mode, false for non-interactive
InitialPrompt string // initial prompt for non-interactive mode
ContinueAfterRun bool // true to continue to interactive mode after initial run (--no-exit)
ApproveToolRun bool // only used in interactive mode
// UI configuration
Quiet bool // suppress all output except final response
@@ -1103,7 +1110,27 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
currentSpinner.Start()
}
},
streamingCallback, // Add streaming callback as the last parameter
// Add streaming callback handler
streamingCallback,
// Tool call approval handler - called before tool execution to get user approval
func(toolName, toolArgs string) (bool, error) {
if !config.IsInteractive || !config.ApproveToolRun {
return true, nil
}
if currentSpinner != nil {
currentSpinner.Stop()
currentSpinner = nil
}
allow, err := cli.GetToolApproval(toolName, toolArgs)
if err != nil {
return false, err
}
// Start spinner again for tool calls
currentSpinner = ui.NewSpinner("Thinking...")
currentSpinner.Start()
return allow, nil
},
)
// Make sure spinner is stopped if still running
@@ -1306,6 +1333,7 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
IsInteractive: false,
InitialPrompt: prompt,
ContinueAfterRun: noExit,
ApproveToolRun: false,
Quiet: quiet,
ServerNames: serverNames,
ToolNames: toolNames,
@@ -1318,12 +1346,13 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
}
// runInteractiveMode handles the interactive mode execution
func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, serverNames, toolNames []string, modelName string, messages []*schema.Message, sessionManager *session.Manager, hookExecutor *hooks.Executor) error {
func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, serverNames, toolNames []string, modelName string, messages []*schema.Message, sessionManager *session.Manager, hookExecutor *hooks.Executor, approveToolRun bool) error {
// Configure and run unified agentic loop
config := AgenticLoopConfig{
IsInteractive: true,
InitialPrompt: "",
ContinueAfterRun: false,
ApproveToolRun: approveToolRun,
Quiet: false,
ServerNames: serverNames,
ToolNames: toolNames,
+24 -18
View File
@@ -7,21 +7,21 @@ toolchain go1.24.5
require (
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/bytedance/sonic v1.14.1
github.com/bytedance/sonic v1.15.0
github.com/charmbracelet/fang v0.4.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/cloudwego/eino v0.5.0-alpha.11
github.com/cloudwego/eino-ext/components/model/claude v0.1.0
github.com/cloudwego/eino-ext/components/model/ollama v0.1.2
github.com/cloudwego/eino v0.7.13
github.com/cloudwego/eino-ext/components/model/claude v0.1.12
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845
github.com/getkin/kin-openapi v0.120.0
github.com/eino-contrib/jsonschema v1.0.3
github.com/getkin/kin-openapi v0.131.0
github.com/mark3labs/mcp-filesystem-server v0.11.1
github.com/mark3labs/mcp-go v0.43.0
github.com/ollama/ollama v0.11.8
github.com/mark3labs/mcp-go v0.44.0-beta.2
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.20.1
github.com/tidwall/gjson v1.18.0
golang.org/x/term v0.34.0
golang.org/x/term v0.37.0
google.golang.org/genai v1.22.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -29,6 +29,7 @@ require (
require (
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
@@ -52,7 +53,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 // indirect
@@ -65,7 +66,7 @@ require (
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.0 // indirect
github.com/eino-contrib/ollama v0.1.0 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -85,7 +86,6 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@@ -101,6 +101,8 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -122,13 +124,17 @@ require (
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.246.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
@@ -137,7 +143,7 @@ require (
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
@@ -153,7 +159,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.31.0 // indirect
)
+51 -38
View File
@@ -2,6 +2,8 @@ cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
@@ -71,15 +73,15 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8=
github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/fang v0.4.0 h1:boBxmdcFghTeotqkD2itXi7SMBozdIlcslRqjboSJDg=
@@ -108,12 +110,12 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.5.0-alpha.11 h1:KhjJ8JTAI/Ed5iCHWKUn1v4j1sDCxqV26HRoUQpSRFc=
github.com/cloudwego/eino v0.5.0-alpha.11/go.mod h1:S38tlNO4cNqFfGJKQSJZimxjzc9JDJKdf2eW3FEEfdc=
github.com/cloudwego/eino-ext/components/model/claude v0.1.0 h1:UZVwYzV7gOBCBKHGdAT2fZzm/+2TBEfDDYn713EvLF0=
github.com/cloudwego/eino-ext/components/model/claude v0.1.0/go.mod h1:lacy0WE3yKuOSxrhJQKqWAxn3LiUy/CJ91jU7nLDNNQ=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.2 h1:WxJ+7oXnr3AhM6u4VbFF3L2ionxCrPfmLetx7V+zthw=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.2/go.mod h1:OgGMCiR/G/RnOWaJvdK8pVSxAzoz2SlCqim43oFTuwo=
github.com/cloudwego/eino v0.7.13 h1:Ku7hY+83gGJJjf4On3UgqjC57UcA+DXe0tqAZiNDDew=
github.com/cloudwego/eino v0.7.13/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino-ext/components/model/claude v0.1.12 h1:c66gFH9J5Ku2/v1f7jPwI9R4CYw5TiAlIVzsfzjsF1g=
github.com/cloudwego/eino-ext/components/model/claude v0.1.12/go.mod h1:a9oQkf4Ib+/VqjsLRdRETytt2m/C4fbcvfjPNu6nVAg=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8 h1:+BStnQlkRxWMV9jsPopLmmut2ARG88e9hDSMaDNAI/w=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8/go.mod h1:C3rf3yy2nEoXFP/CQJne4gbiu1pREKplHKmFlhuOzPE=
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845 h1:nxflfiBwWNPoKS9X4SMhmT+si7rtYv+lQzIyPJik4DM=
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250903035842-96774a3ec845/go.mod h1:QQhCuQxuBAVWvu/YAZBhs/RsR76mUigw59Tl0kh04C8=
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250826113018-8c6f6358d4bb h1:RMslzyijc3bi9EkqCulpS0hZupTl1y/wayR3+fVRN/c=
@@ -128,8 +130,10 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eino-contrib/jsonschema v1.0.0 h1:dXxbhGNZuI3+xNi8x3JT8AGyoXz6Pff6mRvmpjVl5Ww=
github.com/eino-contrib/jsonschema v1.0.0/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
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/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
@@ -143,8 +147,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg=
github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
@@ -194,8 +198,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -220,8 +222,8 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-filesystem-server v0.11.1 h1:7uKIZRMaKWfgvtDj/uLAvo0+7Mwb8gxo5DJywhqFW88=
github.com/mark3labs/mcp-filesystem-server v0.11.1/go.mod h1:xDqJizVYWZ5a31Mt4xuYbVku2AR/kT56H3O0SbpANoQ=
github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA=
github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mark3labs/mcp-go v0.44.0-beta.2 h1:gfUT0m77E4odfgiHkqV/E+MQVaQ06rbutW7Ln0JRkBA=
github.com/mark3labs/mcp-go v0.44.0-beta.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -262,8 +264,10 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/ollama/ollama v0.11.8 h1:S7INjNBa7eGm87zfO/LWfP1ov8NMEuB6OOKgDGMAA9s=
github.com/ollama/ollama v0.11.8/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@@ -318,13 +322,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -361,6 +367,8 @@ github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
@@ -375,8 +383,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -385,8 +393,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -406,8 +414,10 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -416,8 +426,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -437,8 +447,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -450,8 +460,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -461,8 +471,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -472,6 +484,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.246.0 h1:H0ODDs5PnMZVZAEtdLMn2Ul2eQi7QNjqM2DIFp8TlTM=
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
google.golang.org/genai v1.22.0 h1:5hrEhXXWJQZa3tdPocl4vQ/0w6myEAxdNns2Kmx0f4Y=
google.golang.org/genai v1.22.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
@@ -491,6 +505,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+25 -7
View File
@@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
@@ -12,8 +15,6 @@ import (
"github.com/mark3labs/mcphost/internal/config"
"github.com/mark3labs/mcphost/internal/models"
"github.com/mark3labs/mcphost/internal/tools"
"strings"
"time"
)
// AgentConfig holds configuration options for creating a new Agent.
@@ -57,6 +58,10 @@ type StreamingResponseHandler func(content string)
// It receives any text content that the model generates alongside tool calls.
type ToolCallContentHandler func(content string)
// ToolApprovalHandler is a function type for handling user approval of tool calls.
// It receives the tool name and arguments, and returns true if the user approves.
type ToolApprovalHandler func(toolName, toolArgs string) (bool, error)
// Agent represents an AI agent with MCP tool integration and real-time tool call display.
// It manages the interaction between an LLM and various tools through the MCP protocol.
type Agent struct {
@@ -128,17 +133,17 @@ type GenerateWithLoopResult struct {
// It handles the conversation flow, executing tools as needed and invoking callbacks for various events.
// This method does not support streaming responses; use GenerateWithLoopAndStreaming for streaming support.
func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message,
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler) (*GenerateWithLoopResult, error) {
return a.GenerateWithLoopAndStreaming(ctx, messages, onToolCall, onToolExecution, onToolResult, onResponse, onToolCallContent, nil)
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onToolApproval ToolApprovalHandler,
) (*GenerateWithLoopResult, error) {
return a.GenerateWithLoopAndStreaming(ctx, messages, onToolCall, onToolExecution, onToolResult, onResponse, onToolCallContent, nil, onToolApproval)
}
// GenerateWithLoopAndStreaming processes messages with a custom loop that displays tool calls in real-time and supports streaming callbacks.
// It handles the conversation flow, executing tools as needed and invoking callbacks for various events including streaming chunks.
// The onStreamingResponse callback is invoked for each content chunk during streaming if streaming is enabled.
func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []*schema.Message,
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onStreamingResponse StreamingResponseHandler) (*GenerateWithLoopResult, error) {
onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler, onStreamingResponse StreamingResponseHandler, onToolApproval ToolApprovalHandler,
) (*GenerateWithLoopResult, error) {
// Create a copy of messages to avoid modifying the original
workingMessages := make([]*schema.Message, len(messages))
copy(workingMessages, messages)
@@ -200,6 +205,19 @@ func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []*sc
// Handle tool calls
for _, toolCall := range response.ToolCalls {
if onToolApproval != nil {
approved, err := onToolApproval(toolCall.Function.Name, toolCall.Function.Arguments)
if err != nil {
return nil, err
}
if !approved {
rejectedMsg := fmt.Sprintf("The user did not allow tool call %s. Reason: User cancelled.", toolCall.Function.Name)
toolMessage := schema.ToolMessage(rejectedMsg, toolCall.ID)
workingMessages = append(workingMessages, toolMessage)
continue
}
}
// Notify about tool call
if onToolCall != nil {
onToolCall(toolCall.Function.Name, toolCall.Function.Arguments)
+1
View File
@@ -166,6 +166,7 @@ type Config struct {
Stream *bool `json:"stream,omitempty" yaml:"stream,omitempty"`
Theme any `json:"theme" yaml:"theme"`
MarkdownTheme any `json:"markdown-theme" yaml:"markdown-theme"`
ApproveToolRun bool `json:"approve-tool-run" yaml:"approve-tool-run"`
// Model generation parameters
MaxTokens int `json:"max-tokens,omitempty" yaml:"max-tokens,omitempty"`
+13 -15
View File
@@ -11,6 +11,7 @@ import (
"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
"github.com/eino-contrib/jsonschema"
"github.com/getkin/kin-openapi/openapi3"
"google.golang.org/genai"
)
@@ -83,7 +84,7 @@ type Config struct {
// ResponseSchema defines the structure for JSON responses
// Optional. Used when you want structured output in JSON format
ResponseSchema *openapi3.Schema
ResponseSchema *jsonschema.Schema
// EnableCodeExecution allows the model to execute code
// Warning: Be cautious with code execution in production
@@ -103,7 +104,7 @@ type options struct {
// TopK limits the number of tokens to sample from
TopK *int32
// ResponseSchema defines the expected JSON structure for responses
ResponseSchema *openapi3.Schema
ResponseSchema *jsonschema.Schema
}
// ChatModel implements the Gemini chat model for the eino framework.
@@ -124,7 +125,7 @@ type ChatModel struct {
// topK limits token sampling
topK *int32
// responseSchema for structured JSON output
responseSchema *openapi3.Schema
responseSchema *jsonschema.Schema
// tools converted to Gemini format
tools []*genai.Tool
// origTools stores the original tool definitions
@@ -448,7 +449,7 @@ func (cm *ChatModel) buildGenerateConfig(opts ...model.Option) (*genai.GenerateC
// Set response schema for JSON mode
if geminiOptions.ResponseSchema != nil {
gSchema, err := cm.convertOpenAPISchema(geminiOptions.ResponseSchema)
gSchema, err := cm.convertJSONSchema(geminiOptions.ResponseSchema)
if err != nil {
return nil, nil, fmt.Errorf("convert response schema failed: %w", err)
}
@@ -466,12 +467,12 @@ func (cm *ChatModel) convertToGeminiTools(tools []*schema.ToolInfo) ([]*genai.To
var functionDeclarations []*genai.FunctionDeclaration
for _, tool := range tools {
openSchema, err := tool.ToOpenAPIV3()
openSchema, err := tool.ToJSONSchema()
if err != nil {
return nil, fmt.Errorf("get open schema failed: %w", err)
}
gSchema, err := cm.convertOpenAPISchema(openSchema)
gSchema, err := cm.convertJSONSchema(openSchema)
if err != nil {
return nil, fmt.Errorf("convert open schema failed: %w", err)
}
@@ -487,7 +488,7 @@ func (cm *ChatModel) convertToGeminiTools(tools []*schema.ToolInfo) ([]*genai.To
return []*genai.Tool{{FunctionDeclarations: functionDeclarations}}, nil
}
func (cm *ChatModel) convertOpenAPISchema(schema *openapi3.Schema) (*genai.Schema, error) {
func (cm *ChatModel) convertJSONSchema(schema *jsonschema.Schema) (*genai.Schema, error) {
if schema == nil {
return nil, nil
}
@@ -501,15 +502,12 @@ func (cm *ChatModel) convertOpenAPISchema(schema *openapi3.Schema) (*genai.Schem
result.Type = genai.TypeObject
if schema.Properties != nil {
properties := make(map[string]*genai.Schema)
for name, prop := range schema.Properties {
if prop == nil || prop.Value == nil {
continue
}
propSchema, err := cm.convertOpenAPISchema(prop.Value)
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
propSchema, err := cm.convertJSONSchema(pair.Value)
if err != nil {
return nil, err
}
properties[name] = propSchema
properties[pair.Key] = propSchema
}
result.Properties = properties
}
@@ -518,8 +516,8 @@ func (cm *ChatModel) convertOpenAPISchema(schema *openapi3.Schema) (*genai.Schem
}
case openapi3.TypeArray:
result.Type = genai.TypeArray
if schema.Items != nil && schema.Items.Value != nil {
itemSchema, err := cm.convertOpenAPISchema(schema.Items.Value)
if schema.Items != nil {
itemSchema, err := cm.convertJSONSchema(schema.Items)
if err != nil {
return nil, err
}
+33431 -14495
View File
File diff suppressed because it is too large Load Diff
+73 -7
View File
@@ -19,7 +19,6 @@ import (
"github.com/mark3labs/mcphost/internal/models/anthropic"
"github.com/mark3labs/mcphost/internal/models/openai"
"github.com/mark3labs/mcphost/internal/ui/progress"
"github.com/ollama/ollama/api"
"google.golang.org/genai"
"github.com/mark3labs/mcphost/internal/auth"
@@ -218,6 +217,12 @@ func CreateProvider(ctx context.Context, config *ProviderConfig) (*ProviderResul
return nil, err
}
return &ProviderResult{Model: model, Message: ""}, nil
case "google-vertex-anthropic":
model, err := createVertexAnthropicProvider(ctx, config, modelName)
if err != nil {
return nil, err
}
return &ProviderResult{Model: model, Message: ""}, nil
default:
return nil, fmt.Errorf("unsupported provider: %s", provider)
}
@@ -335,6 +340,67 @@ func createAnthropicProvider(ctx context.Context, config *ProviderConfig, modelN
claudeConfig.Temperature = config.Temperature
}
if config.TopP != nil && *config.TopP != 0.95 {
claudeConfig.TopP = config.TopP
}
if config.TopK != nil {
claudeConfig.TopK = config.TopK
}
if len(config.StopSequences) > 0 {
claudeConfig.StopSequences = config.StopSequences
}
return anthropic.NewCustomChatModel(ctx, claudeConfig)
}
func createVertexAnthropicProvider(ctx context.Context, config *ProviderConfig, modelName string) (model.ToolCallingChatModel, error) {
projectID := os.Getenv("GOOGLE_VERTEX_PROJECT")
if projectID == "" {
projectID = os.Getenv("ANTHROPIC_VERTEX_PROJECT_ID")
}
if projectID == "" {
projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
}
if projectID == "" {
projectID = os.Getenv("GCLOUD_PROJECT")
}
if projectID == "" {
projectID = os.Getenv("CLOUDSDK_CORE_PROJECT")
}
if projectID == "" {
return nil, fmt.Errorf("Google Vertex project ID not provided. Set ANTHROPIC_VERTEX_PROJECT_ID, GOOGLE_CLOUD_PROJECT, or GCLOUD_PROJECT environment variable")
}
region := os.Getenv("GOOGLE_VERTEX_LOCATION")
if region == "" {
region = os.Getenv("ANTHROPIC_VERTEX_REGION")
}
if region == "" {
region = os.Getenv("CLOUD_ML_REGION")
}
if region == "" {
region = "global"
}
maxTokens := config.MaxTokens
if maxTokens == 0 {
maxTokens = 4096 // Default value
}
claudeConfig := &einoclaude.Config{
ByVertex: true,
VertexProjectID: projectID,
VertexRegion: region,
Model: modelName,
MaxTokens: maxTokens,
}
if config.Temperature != nil {
claudeConfig.Temperature = config.Temperature
}
if config.TopP != nil {
claudeConfig.TopP = config.TopP
}
@@ -473,7 +539,7 @@ func createGoogleProvider(ctx context.Context, config *ProviderConfig, modelName
type OllamaLoadingResult struct {
// Options contains the actual Ollama options used for loading
// May differ from requested options if fallback occurred (e.g., CPU instead of GPU)
Options *api.Options
Options *ollama.Options
// Message describes the loading result
// Example: "Model loaded successfully on GPU" or
@@ -482,7 +548,7 @@ type OllamaLoadingResult struct {
}
// loadOllamaModelWithFallback loads an Ollama model with GPU settings and automatic CPU fallback
func loadOllamaModelWithFallback(ctx context.Context, baseURL, modelName string, options *api.Options, tlsSkipVerify bool) (*OllamaLoadingResult, error) {
func loadOllamaModelWithFallback(ctx context.Context, baseURL, modelName string, options *ollama.Options, tlsSkipVerify bool) (*OllamaLoadingResult, error) {
client := createHTTPClientWithTLSConfig(tlsSkipVerify)
// Phase 1: Check if model exists locally
@@ -590,7 +656,7 @@ func pullOllamaModelWithProgress(ctx context.Context, client *http.Client, baseU
}
// loadOllamaModelWithOptions loads a model with specific options using a warmup request
func loadOllamaModelWithOptions(ctx context.Context, client *http.Client, baseURL, modelName string, options *api.Options) (*api.Options, error) {
func loadOllamaModelWithOptions(ctx context.Context, client *http.Client, baseURL, modelName string, options *ollama.Options) (*ollama.Options, error) {
// Create a copy of options for warmup to avoid modifying the original
warmupOptions := *options
warmupOptions.NumPredict = 1 // Limit response length for warmup
@@ -667,8 +733,8 @@ func createOllamaProviderWithResult(ctx context.Context, config *ProviderConfig,
baseURL = config.ProviderURL
}
// Set up options for Ollama using the api.Options struct
options := &api.Options{}
// Set up options for Ollama using the ollama.Options struct
options := &ollama.Options{}
if config.Temperature != nil {
options.Temperature = *config.Temperature
@@ -700,7 +766,7 @@ func createOllamaProviderWithResult(ctx context.Context, config *ProviderConfig,
}
// Create a clean copy of options for the final model
finalOptions := &api.Options{}
finalOptions := &ollama.Options{}
*finalOptions = *options // Copy all fields
// Try to pre-load the model with GPU settings and automatic CPU fallback
+13
View File
@@ -100,6 +100,19 @@ func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) err
return nil
}
// Add alternative environment variable names for google-vertex-anthropic
// These match the env vars checked by eino-claude and other tools
if provider == "google-vertex-anthropic" {
envVars = append(envVars,
"ANTHROPIC_VERTEX_PROJECT_ID",
"GOOGLE_CLOUD_PROJECT",
"GCLOUD_PROJECT",
"CLOUDSDK_CORE_PROJECT",
"ANTHROPIC_VERTEX_REGION",
"CLOUD_ML_REGION",
)
}
// Check if at least one environment variable is set
var foundVar bool
for _, envVar := range envVars {
+89 -4
View File
@@ -11,7 +11,7 @@ import (
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/getkin/kin-openapi/openapi3"
"github.com/eino-contrib/jsonschema"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
@@ -221,7 +221,14 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
if err != nil {
return fmt.Errorf("conv mcp tool input schema fail(marshal): %w, tool name: %s", err, mcpTool.Name)
}
inputSchema := &openapi3.Schema{}
// Fix for JSON Schema draft-07 vs draft-04 compatibility:
// Chrome DevTools MCP uses draft-07 where exclusiveMinimum/exclusiveMaximum are numbers,
// but kin-openapi (OpenAPI 3.0) expects them as booleans (draft-04 format).
// Pre-process the schema to convert numeric exclusive bounds to boolean format.
marshaledInputSchema = convertExclusiveBoundsToBoolean(marshaledInputSchema)
inputSchema := &jsonschema.Schema{}
err = sonic.Unmarshal(marshaledInputSchema, inputSchema)
if err != nil {
return fmt.Errorf("conv mcp tool input schema fail(unmarshal): %w, tool name: %s", err, mcpTool.Name)
@@ -231,7 +238,7 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
// OpenAI function calling requires object schemas to have a "properties" field
// even if it's empty, otherwise it throws "object schema missing properties" error
if inputSchema.Type == "object" && inputSchema.Properties == nil {
inputSchema.Properties = make(openapi3.Schemas)
inputSchema.Properties = jsonschema.NewProperties()
}
// Create prefixed tool name
@@ -251,7 +258,7 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
info: &schema.ToolInfo{
Name: prefixedName,
Desc: mcpTool.Description,
ParamsOneOf: schema.NewParamsOneOfByOpenAPIV3(inputSchema),
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(inputSchema),
},
mapping: mapping,
}
@@ -555,3 +562,81 @@ func (m *MCPToolManager) debugLogConnectionInfo(serverName string, serverConfig
}
}
}
// convertExclusiveBoundsToBoolean converts JSON Schema draft-07 style exclusive bounds
// (where exclusiveMinimum/exclusiveMaximum are numbers) to draft-04 style
// (where they are booleans that modify minimum/maximum).
// This enables compatibility with kin-openapi which uses OpenAPI 3.0 (draft-04 based) schemas.
func convertExclusiveBoundsToBoolean(schemaJSON []byte) []byte {
var data map[string]interface{}
if err := json.Unmarshal(schemaJSON, &data); err != nil {
return schemaJSON // Return unchanged on error
}
convertSchemaRecursive(data)
result, err := json.Marshal(data)
if err != nil {
return schemaJSON // Return unchanged on error
}
return result
}
// convertSchemaRecursive recursively processes a schema map and converts
// numeric exclusiveMinimum/exclusiveMaximum to boolean format.
func convertSchemaRecursive(schema map[string]interface{}) {
// Convert exclusiveMinimum if it's a number
if exMin, ok := schema["exclusiveMinimum"]; ok {
if num, isNum := exMin.(float64); isNum {
// JSON Schema draft-07: exclusiveMinimum is the limit value
// Convert to draft-04: set minimum = value, exclusiveMinimum = true
schema["minimum"] = num
schema["exclusiveMinimum"] = true
}
}
// Convert exclusiveMaximum if it's a number
if exMax, ok := schema["exclusiveMaximum"]; ok {
if num, isNum := exMax.(float64); isNum {
// JSON Schema draft-07: exclusiveMaximum is the limit value
// Convert to draft-04: set maximum = value, exclusiveMaximum = true
schema["maximum"] = num
schema["exclusiveMaximum"] = true
}
}
// Recursively process properties
if props, ok := schema["properties"].(map[string]interface{}); ok {
for _, prop := range props {
if propSchema, ok := prop.(map[string]interface{}); ok {
convertSchemaRecursive(propSchema)
}
}
}
// Recursively process items (for arrays)
if items, ok := schema["items"].(map[string]interface{}); ok {
convertSchemaRecursive(items)
}
// Recursively process additionalProperties
if addProps, ok := schema["additionalProperties"].(map[string]interface{}); ok {
convertSchemaRecursive(addProps)
}
// Recursively process allOf, anyOf, oneOf
for _, key := range []string{"allOf", "anyOf", "oneOf"} {
if arr, ok := schema[key].([]interface{}); ok {
for _, item := range arr {
if itemSchema, ok := item.(map[string]interface{}); ok {
convertSchemaRecursive(itemSchema)
}
}
}
}
// Recursively process not
if not, ok := schema["not"].(map[string]interface{}); ok {
convertSchemaRecursive(not)
}
}
+217 -4
View File
@@ -2,11 +2,12 @@ package tools
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/cloudwego/eino/schema"
"github.com/getkin/kin-openapi/openapi3"
"github.com/eino-contrib/jsonschema"
"github.com/mark3labs/mcphost/internal/config"
)
@@ -131,7 +132,7 @@ func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
// Create a schema that would cause the OpenAI validation error
// This simulates what might happen with tools that have no input properties
brokenSchema := &openapi3.Schema{
brokenSchema := &jsonschema.Schema{
Type: "object",
// Properties is nil - this causes "object schema missing properties" error in OpenAI
}
@@ -143,7 +144,7 @@ func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
// Apply the fix from issue #89
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
brokenSchema.Properties = make(openapi3.Schemas)
brokenSchema.Properties = jsonschema.NewProperties()
}
// Verify the fix worked
@@ -153,12 +154,224 @@ func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
// Test that we can create a ParamsOneOf from the fixed schema
// This is what would fail before the fix
paramsOneOf := schema.NewParamsOneOfByOpenAPIV3(brokenSchema)
paramsOneOf := schema.NewParamsOneOfByJSONSchema(brokenSchema)
if paramsOneOf == nil {
t.Error("Failed to create ParamsOneOf from fixed schema - OpenAI function calling would fail")
}
}
// TestConvertExclusiveBoundsToBoolean tests the JSON Schema draft-07 to draft-04 conversion
// for exclusiveMinimum and exclusiveMaximum fields.
// Draft-07: exclusiveMinimum/exclusiveMaximum are numeric values (the actual bounds)
// Draft-04: exclusiveMinimum/exclusiveMaximum are booleans that modify minimum/maximum
func TestConvertExclusiveBoundsToBoolean(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]interface{}
}{
{
name: "exclusiveMinimum as number",
input: `{"type": "number", "exclusiveMinimum": 0}`,
expected: map[string]interface{}{
"type": "number",
"minimum": float64(0),
"exclusiveMinimum": true,
},
},
{
name: "exclusiveMaximum as number",
input: `{"type": "number", "exclusiveMaximum": 100}`,
expected: map[string]interface{}{
"type": "number",
"maximum": float64(100),
"exclusiveMaximum": true,
},
},
{
name: "both exclusive bounds as numbers",
input: `{"type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 10}`,
expected: map[string]interface{}{
"type": "integer",
"minimum": float64(1),
"exclusiveMinimum": true,
"maximum": float64(10),
"exclusiveMaximum": true,
},
},
{
name: "already boolean exclusiveMinimum (draft-04 style)",
input: `{"type": "number", "minimum": 0, "exclusiveMinimum": true}`,
expected: map[string]interface{}{
"type": "number",
"minimum": float64(0),
"exclusiveMinimum": true,
},
},
{
name: "no exclusive bounds",
input: `{"type": "string", "minLength": 1}`,
expected: map[string]interface{}{
"type": "string",
"minLength": float64(1),
},
},
{
name: "nested properties with exclusive bounds",
input: `{"type": "object", "properties": {"age": {"type": "integer", "exclusiveMinimum": 0}}}`,
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"age": map[string]interface{}{
"type": "integer",
"minimum": float64(0),
"exclusiveMinimum": true,
},
},
},
},
{
name: "array items with exclusive bounds",
input: `{"type": "array", "items": {"type": "number", "exclusiveMaximum": 100}}`,
expected: map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "number",
"maximum": float64(100),
"exclusiveMaximum": true,
},
},
},
{
name: "allOf with exclusive bounds",
input: `{"allOf": [{"type": "number", "exclusiveMinimum": 0}]}`,
expected: map[string]interface{}{
"allOf": []interface{}{
map[string]interface{}{
"type": "number",
"minimum": float64(0),
"exclusiveMinimum": true,
},
},
},
},
{
name: "additionalProperties with exclusive bounds",
input: `{"type": "object", "additionalProperties": {"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 255}}`,
expected: map[string]interface{}{
"type": "object",
"additionalProperties": map[string]interface{}{
"type": "integer",
"minimum": float64(0),
"exclusiveMinimum": true,
"maximum": float64(255),
"exclusiveMaximum": true,
},
},
},
{
name: "Chrome DevTools MCP style schema (real-world example)",
input: `{"type": "object", "properties": {"timeout": {"type": "integer", "exclusiveMinimum": 0}, "quality": {"type": "number", "minimum": 0, "maximum": 100}}}`,
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"timeout": map[string]interface{}{
"type": "integer",
"minimum": float64(0),
"exclusiveMinimum": true,
},
"quality": map[string]interface{}{
"type": "number",
"minimum": float64(0),
"maximum": float64(100),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertExclusiveBoundsToBoolean([]byte(tt.input))
var got map[string]interface{}
if err := json.Unmarshal(result, &got); err != nil {
t.Fatalf("Failed to unmarshal result: %v", err)
}
if !deepEqual(got, tt.expected) {
t.Errorf("convertExclusiveBoundsToBoolean() =\n%v\nwant:\n%v", got, tt.expected)
}
})
}
}
// TestConvertExclusiveBoundsToBoolean_InvalidJSON tests that invalid JSON is returned unchanged
func TestConvertExclusiveBoundsToBoolean_InvalidJSON(t *testing.T) {
invalidJSON := []byte(`{invalid json}`)
result := convertExclusiveBoundsToBoolean(invalidJSON)
if string(result) != string(invalidJSON) {
t.Errorf("Expected invalid JSON to be returned unchanged, got: %s", string(result))
}
}
// deepEqual compares two maps recursively
func deepEqual(a, b map[string]interface{}) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
bv, ok := b[k]
if !ok {
return false
}
switch av := v.(type) {
case map[string]interface{}:
bvm, ok := bv.(map[string]interface{})
if !ok || !deepEqual(av, bvm) {
return false
}
case []interface{}:
bva, ok := bv.([]interface{})
if !ok || !sliceEqual(av, bva) {
return false
}
default:
if v != bv {
return false
}
}
}
return true
}
// sliceEqual compares two slices recursively
func sliceEqual(a, b []interface{}) bool {
if len(a) != len(b) {
return false
}
for i := range a {
switch av := a[i].(type) {
case map[string]interface{}:
bvm, ok := b[i].(map[string]interface{})
if !ok || !deepEqual(av, bvm) {
return false
}
case []interface{}:
bva, ok := b[i].([]interface{})
if !ok || !sliceEqual(av, bva) {
return false
}
default:
if a[i] != b[i] {
return false
}
}
}
return true
}
// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
+17 -3
View File
@@ -13,9 +13,7 @@ import (
"golang.org/x/term"
)
var (
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
)
var promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
// CLI manages the command-line interface for MCPHost, providing message rendering,
// user input handling, and display management. It supports both standard and compact
@@ -377,6 +375,22 @@ func (c *CLI) IsSlashCommand(input string) bool {
return strings.HasPrefix(input, "/")
}
// GetToolApproval asks the user for permission to execute the tool with the given
// arguments. Returns true if the user approves.
func (c *CLI) GetToolApproval(toolName, toolArgs string) (bool, error) {
input := NewToolApprovalInput(toolName, toolArgs, c.width)
p := tea.NewProgram(input)
finalModel, err := p.Run()
if err != nil {
return false, err
}
if finalInput, ok := finalModel.(*ToolApprovalInput); ok {
return finalInput.approved, nil
}
return false, fmt.Errorf("GetToolApproval: unexpected error type")
}
// SlashCommandResult encapsulates the outcome of processing a slash command,
// indicating whether the command was recognized and handled, and whether the
// conversation history should be cleared as a result of the command.
+135
View File
@@ -0,0 +1,135 @@
package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type ToolApprovalInput struct {
textarea textarea.Model
toolName string
toolArgs string
width int
selected bool // true when "yes" is highlighted and false when "no" is
approved bool
done bool
}
func NewToolApprovalInput(toolName, toolArgs string, width int) *ToolApprovalInput {
ta := textarea.New()
ta.Placeholder = ""
ta.ShowLineNumbers = false
ta.CharLimit = 1000
ta.SetWidth(width - 8) // Account for container padding, border and internal padding
ta.SetHeight(4) // Default to 3 lines like huh
ta.Focus()
// Style the textarea to match huh theme
ta.FocusedStyle.Base = lipgloss.NewStyle()
ta.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ta.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
ta.FocusedStyle.Prompt = lipgloss.NewStyle()
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
return &ToolApprovalInput{
textarea: ta,
toolName: toolName,
toolArgs: toolArgs,
width: width,
selected: true,
}
}
func (t *ToolApprovalInput) Init() tea.Cmd {
return textarea.Blink
}
func (t *ToolApprovalInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "y", "Y":
t.approved = true
t.done = true
return t, tea.Quit
case "n", "N":
t.approved = false
t.done = true
return t, tea.Quit
case "left":
t.selected = true
return t, nil
case "right":
t.selected = false
return t, nil
case "enter":
t.approved = t.selected
t.done = true
return t, tea.Quit
case "esc", "ctrl+c":
t.approved = false
t.done = true
return t, tea.Quit
}
}
return t, nil
}
func (t *ToolApprovalInput) View() string {
if t.done {
return "we are done"
}
// Add left padding to entire component (2 spaces like other UI elements)
containerStyle := lipgloss.NewStyle().PaddingLeft(2)
// Title
titleStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("252")).
MarginBottom(1)
// Input box with huh-like styling
inputBoxStyle := lipgloss.NewStyle().
Border(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(false).
BorderTop(false).
BorderBottom(false).
BorderForeground(lipgloss.Color("39")).
PaddingLeft(1).
Width(t.width - 2) // Account for container padding
// Style for the currently selected/highlighted option
selectedStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("42")). // Bright green
Bold(true).
Underline(true)
// Style for the unselected/unhighlighted option
unselectedStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("240")) // Dark gray
// Build the view
var view strings.Builder
view.WriteString(titleStyle.Render("Allow tool execution"))
view.WriteString("\n")
details := fmt.Sprintf("Tool: %s\nArguments: %s\n\n", t.toolName, t.toolArgs)
view.WriteString(details)
view.WriteString("Allow tool execution: ")
var yesText, noText string
if t.selected {
yesText = selectedStyle.Render("[y]es")
noText = unselectedStyle.Render("[n]o")
} else {
yesText = unselectedStyle.Render("[y]es")
noText = selectedStyle.Render("[n]o")
}
view.WriteString(yesText + "/" + noText + "\n")
return containerStyle.Render(inputBoxStyle.Render(view.String()))
}
+2
View File
@@ -142,6 +142,7 @@ func (m *MCPHost) Prompt(ctx context.Context, message string) (string, error) {
nil, // onToolResult
nil, // onResponse
nil, // onToolCallContent
nil, // onToolApproval
)
if err != nil {
return "", err
@@ -181,6 +182,7 @@ func (m *MCPHost) PromptWithCallbacks(
nil, // onResponse
nil, // onToolCallContent
onStreaming,
nil, // onToolApproval
)
if err != nil {
return "", err
+13
View File
@@ -2,12 +2,17 @@ package sdk_test
import (
"context"
"os"
"testing"
"github.com/mark3labs/mcphost/sdk"
)
func TestNew(t *testing.T) {
if os.Getenv("ANTHROPIC_API_KEY") == "" {
t.Skip("Skipping test: ANTHROPIC_API_KEY not set")
}
ctx := context.Background()
// Test default initialization
@@ -23,6 +28,10 @@ func TestNew(t *testing.T) {
}
func TestNewWithOptions(t *testing.T) {
if os.Getenv("ANTHROPIC_API_KEY") == "" {
t.Skip("Skipping test: ANTHROPIC_API_KEY not set")
}
ctx := context.Background()
opts := &sdk.Options{
@@ -43,6 +52,10 @@ func TestNewWithOptions(t *testing.T) {
}
func TestSessionManagement(t *testing.T) {
if os.Getenv("ANTHROPIC_API_KEY") == "" {
t.Skip("Skipping test: ANTHROPIC_API_KEY not set")
}
ctx := context.Background()
host, err := sdk.New(ctx, &sdk.Options{Quiet: true})