From 55d12401492466519a356542e8e47ca8a81603c9 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 12 Feb 2025 13:26:18 -0500 Subject: [PATCH 1/5] :ambulance: Fix adding tool directories --- .vscode/launch.json | 11 ++++++++++- changelog.md | 10 +++++++++- cmd/go-go-mcp/cmds/config.go | 5 +---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 01617cf..7bb38c4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,16 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cmd/go-go-mcp", - "args": ["start", "--transport", "sse", "--profile", "all", "--log-level", "debug", "--port", "3000"], + "args": ["start", "--transport", "sse", "--profile", "html-extraction", "--log-level", "debug", "--port", "3000"], + "cwd": "${workspaceFolder}" + }, + { + "name": "Add HTML Extraction Tool", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/go-go-mcp", + "args": ["config", "add-tool", "html-extraction", "--dir", "~/code/wesen/corporate-headquarters/go-go-mcp/examples/html-extract"], "cwd": "${workspaceFolder}" } ] diff --git a/changelog.md b/changelog.md index 173c3ab..20da20b 100644 --- a/changelog.md +++ b/changelog.md @@ -1022,4 +1022,12 @@ Enhanced parameter validation to return cast values along with validation errors - Modified `CheckValueValidity` to return both the cast value and any validation errors - Added `setReflectValue` method to handle setting reflect values with proper type casting - Updated tests to verify cast values -- Improved error messages for invalid choices \ No newline at end of file +- Improved error messages for invalid choices + +# Improved YAML Editor Value Node Creation + +Refactored YAML editor to have a more maintainable and recursive value node creation system: +- Extracted value node creation logic into a dedicated CreateValueNode method +- Made value creation process recursive for nested structures +- Improved error handling with more specific error messages +- Centralized value node creation logic for better maintainability \ No newline at end of file diff --git a/cmd/go-go-mcp/cmds/config.go b/cmd/go-go-mcp/cmds/config.go index e30b05a..3d157a2 100644 --- a/cmd/go-go-mcp/cmds/config.go +++ b/cmd/go-go-mcp/cmds/config.go @@ -301,10 +301,7 @@ func NewConfigAddToolCommand() *cobra.Command { } if dir != "" { - err = editor.AddToolDirectory(args[0], dir, map[string]interface{}{ - "debug": false, - "verbose": false, - }) + err = editor.AddToolDirectory(args[0], dir, map[string]interface{}{}) if err != nil { return fmt.Errorf("could not add tool directory: %w", err) } From f568048f13096708674429f4859f238150e0abde Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 12 Feb 2025 13:40:24 -0500 Subject: [PATCH 2/5] :tractor: Move the config providers out to the side --- changelog.md | 18 +++- cmd/go-go-mcp/cmds/start.go | 63 ++++--------- .../config-provider}/prompt_provider.go | 13 +-- pkg/prompts/registry.go | 2 + .../config-provider}/tool_provider.go | 92 +++++++++++++++---- pkg/tools/registry.go | 2 + 6 files changed, 123 insertions(+), 67 deletions(-) rename pkg/{config => prompts/providers/config-provider}/prompt_provider.go (94%) rename pkg/{config => tools/providers/config-provider}/tool_provider.go (74%) diff --git a/changelog.md b/changelog.md index 20da20b..fb20f94 100644 --- a/changelog.md +++ b/changelog.md @@ -1030,4 +1030,20 @@ Refactored YAML editor to have a more maintainable and recursive value node crea - Extracted value node creation logic into a dedicated CreateValueNode method - Made value creation process recursive for nested structures - Improved error handling with more specific error messages -- Centralized value node creation logic for better maintainability \ No newline at end of file +- Centralized value node creation logic for better maintainability + +# Refactor Tool Provider Creation + +Extracted tool provider creation logic from start command to config package for better code organization and reusability. + +- Moved tool provider creation logic to pkg/tools/providers/config/ +- Added CreateToolProviderFromConfig and CreateToolProviderFromDirectories functions +- Simplified start command by using the new functions + +# Move Tool Provider to Dedicated Package + +Moved the configuration-based tool provider to a dedicated package for better code organization. + +- Moved tool provider code to pkg/tools/providers/config/ +- Updated imports and type references +- Improved package documentation \ No newline at end of file diff --git a/cmd/go-go-mcp/cmds/start.go b/cmd/go-go-mcp/cmds/start.go index 4625041..f5f9cef 100644 --- a/cmd/go-go-mcp/cmds/start.go +++ b/cmd/go-go-mcp/cmds/start.go @@ -9,7 +9,6 @@ import ( "syscall" "time" - "github.com/go-go-golems/clay/pkg/repositories" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/parameters" @@ -17,7 +16,9 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/go-go-golems/go-go-mcp/pkg" "github.com/go-go-golems/go-go-mcp/pkg/config" + config_provider "github.com/go-go-golems/go-go-mcp/pkg/tools/providers/config-provider" ) type StartCommandSettings struct { @@ -110,64 +111,40 @@ func (c *StartCommand) Run( srv := server.NewServer(log.Logger) // Create tool provider options - toolProviderOptions := []config.ConfigToolProviderOption{ - config.WithDebug(s.Debug), + toolProviderOptions := []config_provider.ConfigToolProviderOption{ + config_provider.WithDebug(s.Debug), } if s.TracingDir != "" { - toolProviderOptions = append(toolProviderOptions, config.WithTracingDir(s.TracingDir)) + toolProviderOptions = append(toolProviderOptions, config_provider.WithTracingDir(s.TracingDir)) } - // Handle configuration file if provided + var toolProvider pkg.ToolProvider + var err error + + // Try to create tool provider from config file first if s.ConfigFile != "" { - cfg, err := config.LoadFromFile(s.ConfigFile) + toolProvider, err = config_provider.CreateToolProviderFromConfig(s.ConfigFile, s.Profile, toolProviderOptions...) if err != nil { if !os.IsNotExist(err) || len(s.Repositories) == 0 { fmt.Fprintf(os.Stderr, "Run 'go-go-mcp config init' to create a starting configuration file, and further edit it with 'go-go-mcp config edit'\n") - return errors.Wrap(err, "failed to load configuration file") - } - // Config file doesn't exist but we have repositories, continue - log.Warn().Str("config", s.ConfigFile).Msg("Configuration file not found, continuing with provided repositories") - } else { - // Determine profile - profile := s.Profile - if profile == "" { - profile = cfg.DefaultProfile + return errors.Wrap(err, "failed to create tool provider from config") } - - toolProviderOptions = append(toolProviderOptions, config.WithConfig(cfg, profile)) + // Config file doesn't exist but we have repositories, continue with directories } } - // Handle repository directories - if len(s.Repositories) > 0 { - directories := []repositories.Directory{} - for _, repoPath := range s.Repositories { - dir := os.ExpandEnv(repoPath) - // check if dir exists - if fi, err := os.Stat(dir); os.IsNotExist(err) || !fi.IsDir() { - log.Warn().Str("path", dir).Msg("Repository directory does not exist or is not a directory") - continue - } - directories = append(directories, repositories.Directory{ - FS: os.DirFS(dir), - RootDirectory: ".", - RootDocDirectory: "doc", - WatchDirectory: dir, - Name: dir, - SourcePrefix: "file", - }) - } - - if len(directories) > 0 { - toolProviderOptions = append(toolProviderOptions, config.WithDirectories(directories)) + // If no tool provider yet and we have repositories, create from directories + if toolProvider == nil && len(s.Repositories) > 0 { + toolProvider, err = config_provider.CreateToolProviderFromDirectories(s.Repositories, toolProviderOptions...) + if err != nil { + return errors.Wrap(err, "failed to create tool provider from directories") } } - // Create and register tool provider - toolProvider, err := config.NewConfigToolProvider(toolProviderOptions...) - if err != nil { - return errors.Wrap(err, "failed to create tool provider") + if toolProvider == nil { + return fmt.Errorf("no valid configuration source found (neither config file nor repositories)") } + srv.GetRegistry().RegisterToolProvider(toolProvider) // Create root context with cancellation diff --git a/pkg/config/prompt_provider.go b/pkg/prompts/providers/config-provider/prompt_provider.go similarity index 94% rename from pkg/config/prompt_provider.go rename to pkg/prompts/providers/config-provider/prompt_provider.go index ab61e77..4f21d64 100644 --- a/pkg/config/prompt_provider.go +++ b/pkg/prompts/providers/config-provider/prompt_provider.go @@ -1,4 +1,4 @@ -package config +package config_provider import ( "os" @@ -8,6 +8,7 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/help" "github.com/go-go-golems/go-go-mcp/pkg" + "github.com/go-go-golems/go-go-mcp/pkg/config" "github.com/go-go-golems/go-go-mcp/pkg/protocol" "github.com/pkg/errors" ) @@ -16,17 +17,17 @@ import ( type ConfigPromptProvider struct { repository *repositories.Repository pinocchioFiles map[string]*protocol.Prompt - promptConfigs map[string]*SourceConfig + promptConfigs map[string]*config.SourceConfig } -func NewConfigPromptProvider(config *Config, profile string) (*ConfigPromptProvider, error) { - if _, ok := config.Profiles[profile]; !ok { +func NewConfigPromptProvider(config_ *config.Config, profile string) (*ConfigPromptProvider, error) { + if _, ok := config_.Profiles[profile]; !ok { return nil, errors.Errorf("profile %s not found", profile) } directories := []repositories.Directory{} - profileConfig := config.Profiles[profile] + profileConfig := config_.Profiles[profile] // Load directories using Clay's repository system for _, dir := range profileConfig.Prompts.Directories { @@ -45,7 +46,7 @@ func NewConfigPromptProvider(config *Config, profile string) (*ConfigPromptProvi provider := &ConfigPromptProvider{ repository: repositories.NewRepository(repositories.WithDirectories(directories...)), pinocchioFiles: make(map[string]*protocol.Prompt), - promptConfigs: make(map[string]*SourceConfig), + promptConfigs: make(map[string]*config.SourceConfig), } if profileConfig.Prompts == nil { diff --git a/pkg/prompts/registry.go b/pkg/prompts/registry.go index c2540c2..c1bc916 100644 --- a/pkg/prompts/registry.go +++ b/pkg/prompts/registry.go @@ -17,6 +17,8 @@ type Registry struct { handlers map[string]Handler } +var _ pkg.PromptProvider = &Registry{} + // Handler is a function that generates a prompt message based on arguments type Handler func(prompt protocol.Prompt, arguments map[string]string) (*protocol.PromptMessage, error) diff --git a/pkg/config/tool_provider.go b/pkg/tools/providers/config-provider/tool_provider.go similarity index 74% rename from pkg/config/tool_provider.go rename to pkg/tools/providers/config-provider/tool_provider.go index a3a41ef..fa1d611 100644 --- a/pkg/config/tool_provider.go +++ b/pkg/tools/providers/config-provider/tool_provider.go @@ -1,4 +1,4 @@ -package config +package config_provider import ( "context" @@ -15,16 +15,18 @@ import ( "github.com/go-go-golems/glazed/pkg/help" "github.com/go-go-golems/go-go-mcp/pkg" mcp_cmds "github.com/go-go-golems/go-go-mcp/pkg/cmds" + "github.com/go-go-golems/go-go-mcp/pkg/config" "github.com/go-go-golems/go-go-mcp/pkg/protocol" - "github.com/go-go-golems/parka/pkg/handlers/config" + parka_config "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) // ConfigToolProvider implements pkg.ToolProvider interface type ConfigToolProvider struct { repository *repositories.Repository shellCommands map[string]cmds.Command - toolConfigs map[string]*SourceConfig + toolConfigs map[string]*config.SourceConfig helpSystem *help.HelpSystem debug bool tracingDir string @@ -33,6 +35,8 @@ type ConfigToolProvider struct { type ConfigToolProviderOption func(*ConfigToolProvider) error +var _ pkg.ToolProvider = &ConfigToolProvider{} + func WithDebug(debug bool) ConfigToolProviderOption { return func(p *ConfigToolProvider) error { p.debug = debug @@ -54,9 +58,9 @@ func WithDirectories(directories []repositories.Directory) ConfigToolProviderOpt } } -func WithConfig(config *Config, profile string) ConfigToolProviderOption { +func WithConfig(config_ *config.Config, profile string) ConfigToolProviderOption { return func(p *ConfigToolProvider) error { - profileConfig, ok := config.Profiles[profile] + profileConfig, ok := config_.Profiles[profile] if !ok { return errors.Errorf("profile %s not found", profile) } @@ -107,7 +111,7 @@ func WithConfig(config *Config, profile string) ConfigToolProviderOption { func NewConfigToolProvider(options ...ConfigToolProviderOption) (*ConfigToolProvider, error) { provider := &ConfigToolProvider{ shellCommands: make(map[string]cmds.Command), - toolConfigs: make(map[string]*SourceConfig), + toolConfigs: make(map[string]*config.SourceConfig), helpSystem: help.NewHelpSystem(), directories: []repositories.Directory{}, } @@ -265,48 +269,102 @@ func (p *ConfigToolProvider) executeCommand(ctx context.Context, cmd cmds.Comman return protocol.NewToolResult(protocol.WithText(text)), nil } -func (p *ConfigToolProvider) createParkaParameterFilter(sourceConfig *SourceConfig) *config.ParameterFilter { - options := []config.ParameterFilterOption{} +func (p *ConfigToolProvider) createParkaParameterFilter(sourceConfig *config.SourceConfig) *parka_config.ParameterFilter { + options := []parka_config.ParameterFilterOption{} // Handle defaults if sourceConfig.Defaults != nil { - layerParams := config.NewLayerParameters() + layerParams := parka_config.NewLayerParameters() for layer, params := range sourceConfig.Defaults { layerParams.Layers[layer] = params } - options = append(options, config.WithReplaceDefaults(layerParams)) + options = append(options, parka_config.WithReplaceDefaults(layerParams)) } // Handle overrides if sourceConfig.Overrides != nil { - layerParams := config.NewLayerParameters() + layerParams := parka_config.NewLayerParameters() for layer, params := range sourceConfig.Overrides { layerParams.Layers[layer] = params } - options = append(options, config.WithReplaceOverrides(layerParams)) + options = append(options, parka_config.WithReplaceOverrides(layerParams)) } // Handle whitelist if sourceConfig.Whitelist != nil { - whitelist := &config.ParameterFilterList{} + whitelist := &parka_config.ParameterFilterList{} for layer, params := range sourceConfig.Whitelist { whitelist.LayerParameters = map[string][]string{ layer: params, } } - options = append(options, config.WithWhitelist(whitelist)) + options = append(options, parka_config.WithWhitelist(whitelist)) } // Handle blacklist if sourceConfig.Blacklist != nil { - blacklist := &config.ParameterFilterList{} + blacklist := &parka_config.ParameterFilterList{} for layer, params := range sourceConfig.Blacklist { blacklist.LayerParameters = map[string][]string{ layer: params, } } - options = append(options, config.WithBlacklist(blacklist)) + options = append(options, parka_config.WithBlacklist(blacklist)) + } + + return parka_config.NewParameterFilter(options...) +} + +// CreateToolProviderFromConfig creates a tool provider from a config file and profile +func CreateToolProviderFromConfig(configFile string, profile string, options ...ConfigToolProviderOption) (*ConfigToolProvider, error) { + // Handle configuration file if provided + if configFile != "" { + cfg, err := config.LoadFromFile(configFile) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to load configuration file") + } + // Config file doesn't exist, continue with other options + log.Warn().Str("config", configFile).Msg("Configuration file not found") + } else { + // Determine profile + if profile == "" { + profile = cfg.DefaultProfile + } + + options = append(options, WithConfig(cfg, profile)) + } + } + + // Create and return tool provider + return NewConfigToolProvider(options...) +} + +// CreateToolProviderFromDirectories creates a tool provider from a list of directories +func CreateToolProviderFromDirectories(directories []string, options ...ConfigToolProviderOption) (*ConfigToolProvider, error) { + if len(directories) > 0 { + dirs := []repositories.Directory{} + for _, repoPath := range directories { + dir := os.ExpandEnv(repoPath) + // check if dir exists + if fi, err := os.Stat(dir); os.IsNotExist(err) || !fi.IsDir() { + log.Warn().Str("path", dir).Msg("Repository directory does not exist or is not a directory") + continue + } + dirs = append(dirs, repositories.Directory{ + FS: os.DirFS(dir), + RootDirectory: ".", + RootDocDirectory: "doc", + WatchDirectory: dir, + Name: dir, + SourcePrefix: "file", + }) + } + + if len(dirs) > 0 { + options = append(options, WithDirectories(dirs)) + } } - return config.NewParameterFilter(options...) + return NewConfigToolProvider(options...) } diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index 7db4f7a..e41d0b3 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -19,6 +19,8 @@ type Registry struct { // Handler is a function that executes a tool with given arguments type Handler func(ctx context.Context, tool Tool, arguments map[string]interface{}) (*protocol.ToolResult, error) +var _ pkg.ToolProvider = &Registry{} + // NewRegistry creates a new tool registry func NewRegistry() *Registry { return &Registry{ From 50073e1211df6597453bd8cb7c5f979c4f7431b2 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 12 Feb 2025 15:13:02 -0500 Subject: [PATCH 3/5] :sparkles: Allow adding files to a repository --- changelog.md | 54 +++++++++++++- cmd/go-go-mcp/cmds/start.go | 55 +++++++------- .../{tool_provider.go => tool-provider.go} | 71 ++++++++++++++----- 3 files changed, 136 insertions(+), 44 deletions(-) rename pkg/tools/providers/config-provider/{tool_provider.go => tool-provider.go} (89%) diff --git a/changelog.md b/changelog.md index fb20f94..5a5fce5 100644 --- a/changelog.md +++ b/changelog.md @@ -1046,4 +1046,56 @@ Moved the configuration-based tool provider to a dedicated package for better co - Moved tool provider code to pkg/tools/providers/config/ - Updated imports and type references -- Improved package documentation \ No newline at end of file +- Improved package documentation + +# Documentation: Repository System Guide + +Added comprehensive documentation for the repository system, including: +- Basic usage guide +- File system watching +- Command organization +- Advanced features and best practices +- Common patterns and examples + +# File Watching Support for ConfigToolProvider + +Added file watching support to automatically reload commands when files change in watched directories. + +- Added Watch method to ConfigToolProvider for monitoring file changes +- Added WithWatch option to enable/disable file watching +- Automatically reload commands when files are changed or removed +- Support for watching multiple directories simultaneously + +# Simplified File Watching Implementation + +Refactored file watching to use Clay repository's built-in functionality: + +- Removed custom file watching implementation in favor of repository's built-in watching +- Simplified ConfigToolProvider by removing watch-specific fields +- Improved code maintainability by reducing duplication +- Maintained thread safety with mutex protection + +# Consistent Path Processing in Repository Watcher + +Refactored path processing in repository watcher to use a shared helper function, ensuring consistent path handling across all operations. + +- Extracted path processing logic into getProcessedPaths helper method +- Reused path processing in both write and remove callbacks +- Improved code maintainability by reducing duplication +- Maintained consistent path handling for command operations + +## Add support for individual files in Repository + +Added ability to load commands from individual files in Repository, alongside directories. This allows for more flexible command loading configurations. + +- Added Files field to Repository struct +- Added WithFiles repository option +- Updated LoadCommands to handle individual files +- Updated LoadCommandsFromInputs to use file support + +## Use Repository File Support in ConfigToolProvider + +Updated ConfigToolProvider to use the Repository's built-in file support instead of handling files manually: +- Removed manual file loading in ConfigToolProvider +- Use WithFiles repository option for individual files +- Simplified code by leveraging Repository functionality \ No newline at end of file diff --git a/cmd/go-go-mcp/cmds/start.go b/cmd/go-go-mcp/cmds/start.go index f5f9cef..723fd91 100644 --- a/cmd/go-go-mcp/cmds/start.go +++ b/cmd/go-go-mcp/cmds/start.go @@ -15,8 +15,8 @@ import ( "github.com/go-go-golems/go-go-mcp/pkg/server" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "golang.org/x/sync/errgroup" - "github.com/go-go-golems/go-go-mcp/pkg" "github.com/go-go-golems/go-go-mcp/pkg/config" config_provider "github.com/go-go-golems/go-go-mcp/pkg/tools/providers/config-provider" ) @@ -113,12 +113,13 @@ func (c *StartCommand) Run( // Create tool provider options toolProviderOptions := []config_provider.ConfigToolProviderOption{ config_provider.WithDebug(s.Debug), + config_provider.WithWatch(true), } if s.TracingDir != "" { toolProviderOptions = append(toolProviderOptions, config_provider.WithTracingDir(s.TracingDir)) } - var toolProvider pkg.ToolProvider + var toolProvider *config_provider.ConfigToolProvider var err error // Try to create tool provider from config file first @@ -147,45 +148,45 @@ func (c *StartCommand) Run( srv.GetRegistry().RegisterToolProvider(toolProvider) - // Create root context with cancellation - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + // Create a context that will be cancelled on SIGINT/SIGTERM + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer stop() - // Set up signal handling - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + g, gctx := errgroup.WithContext(ctx) - // Start server in a goroutine - errChan := make(chan error, 1) - go func() { - // Start server with selected transport + // Start file watcher + g.Go(func() error { + if err := toolProvider.Watch(gctx); err != nil { + log.Error().Err(err).Msg("failed to start file watcher") + return err + } + return nil + }) + + // Start server + g.Go(func() error { var err error switch s.Transport { case "stdio": log.Info().Msg("Starting server with stdio transport") - err = srv.StartStdio(ctx) + err = srv.StartStdio(gctx) case "sse": log.Info().Int("port", s.Port).Msg("Starting server with SSE transport") - err = srv.StartSSE(ctx, s.Port) + err = srv.StartSSE(gctx, s.Port) default: err = fmt.Errorf("invalid transport type: %s", s.Transport) } - errChan <- err - }() - - // Wait for either server error or interrupt signal - select { - case err := <-errChan: if err != nil && err != io.EOF { log.Error().Err(err).Msg("Server error") return err } return nil - case sig := <-sigChan: - log.Info().Str("signal", sig.String()).Msg("Received signal, initiating graceful shutdown") - // Cancel context to initiate shutdown - cancel() - // Create a timeout context for shutdown + }) + + // Add graceful shutdown handler + g.Go(func() error { + <-gctx.Done() + log.Info().Msg("Initiating graceful shutdown") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := srv.Stop(shutdownCtx); err != nil { @@ -194,5 +195,7 @@ func (c *StartCommand) Run( } log.Info().Msg("Server stopped gracefully") return nil - } + }) + + return g.Wait() } diff --git a/pkg/tools/providers/config-provider/tool_provider.go b/pkg/tools/providers/config-provider/tool-provider.go similarity index 89% rename from pkg/tools/providers/config-provider/tool_provider.go rename to pkg/tools/providers/config-provider/tool-provider.go index fa1d611..beefac9 100644 --- a/pkg/tools/providers/config-provider/tool_provider.go +++ b/pkg/tools/providers/config-provider/tool-provider.go @@ -31,6 +31,8 @@ type ConfigToolProvider struct { debug bool tracingDir string directories []repositories.Directory + files []string + watching bool } type ConfigToolProviderOption func(*ConfigToolProvider) error @@ -58,6 +60,13 @@ func WithDirectories(directories []repositories.Directory) ConfigToolProviderOpt } } +func WithFiles(files []string) ConfigToolProviderOption { + return func(p *ConfigToolProvider) error { + p.files = append(p.files, files...) + return nil + } +} + func WithConfig(config_ *config.Config, profile string) ConfigToolProviderOption { return func(p *ConfigToolProvider) error { profileConfig, ok := config_.Profiles[profile] @@ -69,14 +78,15 @@ func WithConfig(config_ *config.Config, profile string) ConfigToolProviderOption return nil } - // Load directories using Clay's repository system + // Load directories + directories := []repositories.Directory{} for _, dir := range profileConfig.Tools.Directories { absPath, err := filepath.Abs(dir.Path) if err != nil { return errors.Wrapf(err, "failed to get absolute path for %s", dir.Path) } - p.directories = append(p.directories, repositories.Directory{ + directories = append(directories, repositories.Directory{ FS: os.DirFS(absPath), RootDirectory: ".", RootDocDirectory: "doc", @@ -85,24 +95,28 @@ func WithConfig(config_ *config.Config, profile string) ConfigToolProviderOption SourcePrefix: "file", }) } + p.directories = directories - // Load individual files as ShellCommands + // Collect file paths + files := []string{} for _, file := range profileConfig.Tools.Files { absPath, err := filepath.Abs(file.Path) if err != nil { return errors.Wrapf(err, "failed to get absolute path for %s", file.Path) } + files = append(files, absPath) + p.toolConfigs[filepath.Base(absPath)] = &file + } - shellCmd, err := mcp_cmds.LoadShellCommand(absPath) - if err != nil { - return errors.Wrapf(err, "failed to load shell command from %s", file.Path) - } + p.files = files - desc := shellCmd.Description() - p.shellCommands[desc.Name] = shellCmd - p.toolConfigs[desc.Name] = &file - } + return nil + } +} +func WithWatch(watch bool) ConfigToolProviderOption { + return func(p *ConfigToolProvider) error { + p.watching = watch return nil } } @@ -114,6 +128,8 @@ func NewConfigToolProvider(options ...ConfigToolProviderOption) (*ConfigToolProv toolConfigs: make(map[string]*config.SourceConfig), helpSystem: help.NewHelpSystem(), directories: []repositories.Directory{}, + files: []string{}, + watching: false, } for _, option := range options { @@ -125,6 +141,7 @@ func NewConfigToolProvider(options ...ConfigToolProviderOption) (*ConfigToolProv // Create repository with collected directories and shell command loader provider.repository = repositories.NewRepository( repositories.WithDirectories(provider.directories...), + repositories.WithFiles(provider.files...), repositories.WithCommandLoader(&mcp_cmds.ShellCommandLoader{}), ) @@ -158,8 +175,8 @@ func ConvertCommandToTool(desc *cmds.CommandDescription) (protocol.Tool, error) func (p *ConfigToolProvider) ListTools(cursor string) ([]protocol.Tool, string, error) { var tools []protocol.Tool - // Get tools from repositories repoCommands := p.repository.CollectCommands([]string{}, true) + for _, cmd := range repoCommands { tool, err := ConvertCommandToTool(cmd.Description()) if err != nil { @@ -192,8 +209,9 @@ func (p *ConfigToolProvider) ListTools(cursor string) ([]protocol.Tool, string, // CallTool implements pkg.ToolProvider interface func (p *ConfigToolProvider) CallTool(ctx context.Context, name string, arguments map[string]interface{}) (*protocol.ToolResult, error) { - // Try repositories first - if cmd, ok := p.repository.GetCommand(name); ok { + cmd, ok := p.repository.GetCommand(name) + + if ok { return p.executeCommand(ctx, cmd, arguments) } @@ -315,6 +333,16 @@ func (p *ConfigToolProvider) createParkaParameterFilter(sourceConfig *config.Sou return parka_config.NewParameterFilter(options...) } +// Watch starts watching all configured directories for changes +func (p *ConfigToolProvider) Watch(ctx context.Context) error { + if !p.watching { + return nil + } + + // Use the repository's built-in watching functionality + return p.repository.Watch(ctx) +} + // CreateToolProviderFromConfig creates a tool provider from a config file and profile func CreateToolProviderFromConfig(configFile string, profile string, options ...ConfigToolProviderOption) (*ConfigToolProvider, error) { // Handle configuration file if provided @@ -336,8 +364,12 @@ func CreateToolProviderFromConfig(configFile string, profile string, options ... } } - // Create and return tool provider - return NewConfigToolProvider(options...) + provider, err := NewConfigToolProvider(options...) + if err != nil { + return nil, err + } + + return provider, nil } // CreateToolProviderFromDirectories creates a tool provider from a list of directories @@ -366,5 +398,10 @@ func CreateToolProviderFromDirectories(directories []string, options ...ConfigTo } } - return NewConfigToolProvider(options...) + provider, err := NewConfigToolProvider(options...) + if err != nil { + return nil, err + } + + return provider, nil } From bf9b8c818e866a190e39767b2901ced5ba70384d Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 12 Feb 2025 16:20:50 -0500 Subject: [PATCH 4/5] :sparkles: Add tools list to call server side commands immediately --- changelog.md | 71 +----- cmd/go-go-mcp/cmds/server/layers/server.go | 108 +++++++++ cmd/go-go-mcp/cmds/server/server.go | 16 ++ cmd/go-go-mcp/cmds/server/tools.go | 242 +++++++++++++++++++++ cmd/go-go-mcp/cmds/start.go | 97 ++------- cmd/go-go-mcp/main.go | 19 +- examples/bio-stuff/fetch-url.yaml | 2 +- go.mod | 4 +- go.sum | 4 +- 9 files changed, 410 insertions(+), 153 deletions(-) create mode 100644 cmd/go-go-mcp/cmds/server/layers/server.go create mode 100644 cmd/go-go-mcp/cmds/server/server.go create mode 100644 cmd/go-go-mcp/cmds/server/tools.go diff --git a/changelog.md b/changelog.md index 5a5fce5..88e40b6 100644 --- a/changelog.md +++ b/changelog.md @@ -1027,6 +1027,7 @@ Enhanced parameter validation to return cast values along with validation errors # Improved YAML Editor Value Node Creation Refactored YAML editor to have a more maintainable and recursive value node creation system: + - Extracted value node creation logic into a dedicated CreateValueNode method - Made value creation process recursive for nested structures - Improved error handling with more specific error messages @@ -1034,68 +1035,16 @@ Refactored YAML editor to have a more maintainable and recursive value node crea # Refactor Tool Provider Creation -Extracted tool provider creation logic from start command to config package for better code organization and reusability. - -- Moved tool provider creation logic to pkg/tools/providers/config/ -- Added CreateToolProviderFromConfig and CreateToolProviderFromDirectories functions -- Simplified start command by using the new functions - -# Move Tool Provider to Dedicated Package - -Moved the configuration-based tool provider to a dedicated package for better code organization. - -- Moved tool provider code to pkg/tools/providers/config/ -- Updated imports and type references -- Improved package documentation - -# Documentation: Repository System Guide - -Added comprehensive documentation for the repository system, including: -- Basic usage guide -- File system watching -- Command organization -- Advanced features and best practices -- Common patterns and examples - -# File Watching Support for ConfigToolProvider - -Added file watching support to automatically reload commands when files change in watched directories. - -- Added Watch method to ConfigToolProvider for monitoring file changes -- Added WithWatch option to enable/disable file watching -- Automatically reload commands when files are changed or removed -- Support for watching multiple directories simultaneously - -# Simplified File Watching Implementation - -Refactored file watching to use Clay repository's built-in functionality: - -- Removed custom file watching implementation in favor of repository's built-in watching -- Simplified ConfigToolProvider by removing watch-specific fields -- Improved code maintainability by reducing duplication -- Maintained thread safety with mutex protection - -# Consistent Path Processing in Repository Watcher - -Refactored path processing in repository watcher to use a shared helper function, ensuring consistent path handling across all operations. - -- Extracted path processing logic into getProcessedPaths helper method -- Reused path processing in both write and remove callbacks -- Improved code maintainability by reducing duplication -- Maintained consistent path handling for command operations - -## Add support for individual files in Repository +Improved code organization by moving tool provider creation to server layer package. -Added ability to load commands from individual files in Repository, alongside directories. This allows for more flexible command loading configurations. +- Moved tool provider creation to server layer package for better organization +- Made CreateToolProvider a public function for reuse +- Updated start command to use the new package function -- Added Files field to Repository struct -- Added WithFiles repository option -- Updated LoadCommands to handle individual files -- Updated LoadCommandsFromInputs to use file support +## Server Tools Commands -## Use Repository File Support in ConfigToolProvider +Added server-side tool management commands for direct interaction with tool providers. -Updated ConfigToolProvider to use the Repository's built-in file support instead of handling files manually: -- Removed manual file loading in ConfigToolProvider -- Use WithFiles repository option for individual files -- Simplified code by leveraging Repository functionality \ No newline at end of file +- Added `server tools list` command to list available tools directly from tool provider +- Added `server tools call` command to call tools directly without starting the server +- Reused server layer for configuration consistency \ No newline at end of file diff --git a/cmd/go-go-mcp/cmds/server/layers/server.go b/cmd/go-go-mcp/cmds/server/layers/server.go new file mode 100644 index 0000000..6a004e0 --- /dev/null +++ b/cmd/go-go-mcp/cmds/server/layers/server.go @@ -0,0 +1,108 @@ +package layers + +import ( + "fmt" + "os" + + "github.com/go-go-golems/glazed/pkg/cmds/layers" + "github.com/go-go-golems/glazed/pkg/cmds/parameters" + "github.com/go-go-golems/go-go-mcp/pkg/config" + config_provider "github.com/go-go-golems/go-go-mcp/pkg/tools/providers/config-provider" + "github.com/pkg/errors" +) + +type ServerSettings struct { + Repositories []string `glazed.parameter:"repositories"` + ConfigFile string `glazed.parameter:"config-file"` + Profile string `glazed.parameter:"profile"` + Debug bool `glazed.parameter:"debug"` + TracingDir string `glazed.parameter:"tracing-dir"` +} + +const ServerLayerSlug = "mcp-server" + +func NewServerParameterLayer() (layers.ParameterLayer, error) { + defaultConfigFile, err := config.GetDefaultProfilesPath() + if err != nil { + return nil, errors.Wrap(err, "failed to get default profiles path") + } + + return layers.NewParameterLayer(ServerLayerSlug, "MCP Server Settings", + layers.WithParameterDefinitions( + parameters.NewParameterDefinition( + "repositories", + parameters.ParameterTypeStringList, + parameters.WithHelp("List of directories containing shell command repositories"), + parameters.WithDefault([]string{}), + ), + parameters.NewParameterDefinition( + "config-file", + parameters.ParameterTypeString, + parameters.WithHelp("Path to the configuration file"), + parameters.WithDefault(defaultConfigFile), + ), + parameters.NewParameterDefinition( + "profile", + parameters.ParameterTypeString, + parameters.WithHelp("Profile to use from the configuration file"), + parameters.WithDefault(""), + ), + parameters.NewParameterDefinition( + "debug", + parameters.ParameterTypeBool, + parameters.WithHelp("Enable debug mode for shell tool provider"), + parameters.WithDefault(false), + ), + parameters.NewParameterDefinition( + "tracing-dir", + parameters.ParameterTypeString, + parameters.WithHelp("Directory to store tool call traces"), + parameters.WithDefault(""), + ), + ), + ) +} + +// CreateToolProvider creates a tool provider from the given server settings +func CreateToolProvider(serverSettings *ServerSettings) (*config_provider.ConfigToolProvider, error) { + // Create tool provider options + toolProviderOptions := []config_provider.ConfigToolProviderOption{ + config_provider.WithDebug(serverSettings.Debug), + config_provider.WithWatch(true), + } + if serverSettings.TracingDir != "" { + toolProviderOptions = append(toolProviderOptions, config_provider.WithTracingDir(serverSettings.TracingDir)) + } + + var toolProvider *config_provider.ConfigToolProvider + var err error + + // Try to create tool provider from config file first + if serverSettings.ConfigFile != "" { + toolProvider, err = config_provider.CreateToolProviderFromConfig( + serverSettings.ConfigFile, + serverSettings.Profile, + toolProviderOptions...) + if err != nil { + if !os.IsNotExist(err) || len(serverSettings.Repositories) == 0 { + fmt.Fprintf(os.Stderr, "Run 'go-go-mcp config init' to create a starting configuration file, and further edit it with 'go-go-mcp config edit'\n") + return nil, errors.Wrap(err, "failed to create tool provider from config") + } + // Config file doesn't exist but we have repositories, continue with directories + } + } + + // If no tool provider yet and we have repositories, create from directories + if toolProvider == nil && len(serverSettings.Repositories) > 0 { + toolProvider, err = config_provider.CreateToolProviderFromDirectories(serverSettings.Repositories, toolProviderOptions...) + if err != nil { + return nil, errors.Wrap(err, "failed to create tool provider from directories") + } + } + + if toolProvider == nil { + return nil, fmt.Errorf("no valid configuration source found (neither config file nor repositories)") + } + + return toolProvider, nil +} diff --git a/cmd/go-go-mcp/cmds/server/server.go b/cmd/go-go-mcp/cmds/server/server.go new file mode 100644 index 0000000..67c6127 --- /dev/null +++ b/cmd/go-go-mcp/cmds/server/server.go @@ -0,0 +1,16 @@ +package server + +import ( + "github.com/spf13/cobra" +) + +// ServerCmd is the root command for server-related operations +var ServerCmd = &cobra.Command{ + Use: "server", + Short: "Server management commands", + Long: `Commands for managing the MCP server, including starting the server and managing tools.`, +} + +func init() { + ServerCmd.AddCommand(ToolsCmd) +} diff --git a/cmd/go-go-mcp/cmds/server/tools.go b/cmd/go-go-mcp/cmds/server/tools.go new file mode 100644 index 0000000..85ffccf --- /dev/null +++ b/cmd/go-go-mcp/cmds/server/tools.go @@ -0,0 +1,242 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/go-go-golems/glazed/pkg/cli" + "github.com/go-go-golems/glazed/pkg/cmds" + glazed_layers "github.com/go-go-golems/glazed/pkg/cmds/layers" + "github.com/go-go-golems/glazed/pkg/cmds/parameters" + "github.com/go-go-golems/glazed/pkg/middlewares" + "github.com/go-go-golems/glazed/pkg/settings" + "github.com/go-go-golems/glazed/pkg/types" + "github.com/go-go-golems/go-go-mcp/cmd/go-go-mcp/cmds/server/layers" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// ToolsCmd handles the "server tools" command group +var ToolsCmd = &cobra.Command{ + Use: "tools", + Short: "Manage server tools", + Long: `List and call tools directly on the server side.`, +} + +type ListToolsCommand struct { + *cmds.CommandDescription +} + +type ListToolsSettings struct { +} + +type CallToolCommand struct { + *cmds.CommandDescription +} + +type CallToolSettings struct { + ToolName string `glazed.parameter:"tool-name"` + JSON string `glazed.parameter:"json"` + Args map[string]interface{} `glazed.parameter:"args"` +} + +func NewListToolsCommand() (*ListToolsCommand, error) { + glazedParameterLayer, err := settings.NewGlazedParameterLayers() + if err != nil { + return nil, errors.Wrap(err, "could not create Glazed parameter layer") + } + + serverLayer, err := layers.NewServerParameterLayer() + if err != nil { + return nil, errors.Wrap(err, "could not create server parameter layer") + } + + return &ListToolsCommand{ + CommandDescription: cmds.NewCommandDescription( + "list", + cmds.WithShort("List available tools"), + cmds.WithLayersList( + glazedParameterLayer, + serverLayer, + ), + ), + }, nil +} + +func NewCallToolCommand() (*CallToolCommand, error) { + serverLayer, err := layers.NewServerParameterLayer() + if err != nil { + return nil, errors.Wrap(err, "could not create server parameter layer") + } + + return &CallToolCommand{ + CommandDescription: cmds.NewCommandDescription( + "call", + cmds.WithShort("Call a specific tool"), + cmds.WithArguments( + parameters.NewParameterDefinition( + "tool-name", + parameters.ParameterTypeString, + parameters.WithHelp("Name of the tool to call"), + parameters.WithRequired(true), + ), + ), + cmds.WithFlags( + parameters.NewParameterDefinition( + "json", + parameters.ParameterTypeString, + parameters.WithHelp("Tool arguments as JSON string"), + parameters.WithDefault(""), + ), + parameters.NewParameterDefinition( + "json-file", + parameters.ParameterTypeStringFromFile, + parameters.WithHelp("Tool arguments as JSON file"), + parameters.WithDefault(""), + ), + parameters.NewParameterDefinition( + "args", + parameters.ParameterTypeKeyValue, + parameters.WithHelp("Tool arguments as key=value pairs"), + parameters.WithDefault(map[string]interface{}{}), + ), + ), + cmds.WithLayersList(serverLayer), + ), + }, nil +} + +func (c *ListToolsCommand) RunIntoGlazeProcessor( + ctx context.Context, + parsedLayers *glazed_layers.ParsedLayers, + gp middlewares.Processor, +) error { + s := &ListToolsSettings{} + if err := parsedLayers.InitializeStruct(glazed_layers.DefaultSlug, s); err != nil { + return err + } + + serverSettings := &layers.ServerSettings{} + if err := parsedLayers.InitializeStruct(layers.ServerLayerSlug, serverSettings); err != nil { + return err + } + + toolProvider, err := layers.CreateToolProvider(serverSettings) + if err != nil { + return err + } + + tools, cursor, err := toolProvider.ListTools("") + if err != nil { + return err + } + + for _, tool := range tools { + // First unmarshal the schema into an interface{} to ensure it's valid JSON + var schemaObj interface{} + if err := json.Unmarshal(tool.InputSchema, &schemaObj); err != nil { + return fmt.Errorf("failed to parse schema JSON: %w", err) + } + + row := types.NewRow( + types.MRP("name", tool.Name), + types.MRP("description", tool.Description), + types.MRP("schema", schemaObj), + ) + if err := gp.AddRow(ctx, row); err != nil { + return err + } + } + + if cursor != "" { + row := types.NewRow( + types.MRP("cursor", cursor), + types.MRP("type", "cursor"), + ) + if err := gp.AddRow(ctx, row); err != nil { + return err + } + } + + return nil +} + +func (c *CallToolCommand) RunIntoWriter( + ctx context.Context, + parsedLayers *glazed_layers.ParsedLayers, + w io.Writer, +) error { + s := &CallToolSettings{} + if err := parsedLayers.InitializeStruct(glazed_layers.DefaultSlug, s); err != nil { + return err + } + + serverSettings := &layers.ServerSettings{} + if err := parsedLayers.InitializeStruct(layers.ServerLayerSlug, serverSettings); err != nil { + return err + } + + toolProvider, err := layers.CreateToolProvider(serverSettings) + if err != nil { + return err + } + + // Parse tool arguments - first try JSON string, then key-value pairs + toolArgMap := make(map[string]interface{}) + + // If JSON args are provided, they take precedence + if s.JSON != "" { + if err := json.Unmarshal([]byte(s.JSON), &toolArgMap); err != nil { + return fmt.Errorf("invalid tool arguments JSON: %w", err) + } + } else if len(s.Args) > 0 { + // Otherwise use key-value pairs if provided + toolArgMap = s.Args + } + + result, err := toolProvider.CallTool(ctx, s.ToolName, toolArgMap) + if err != nil { + return err + } + + // Pretty print the result + for _, content := range result.Content { + _, err = fmt.Fprintf(w, "Type: %s\n", content.Type) + if err != nil { + return err + } + + switch content.Type { + case "text": + _, err = fmt.Fprintf(w, "Content:\n%s\n", content.Text) + case "image": + _, err = fmt.Fprintf(w, "Image:\n%s\n", content.Data) + case "resource": + _, err = fmt.Fprintf(w, "URI: %s\nMimeType: %s\n", + content.Resource.URI, content.Resource.MimeType) + } + if err != nil { + return err + } + } + return nil +} + +func init() { + listCmd, err := NewListToolsCommand() + cobra.CheckErr(err) + + cobraListCmd, err := cli.BuildCobraCommandFromGlazeCommand(listCmd, cli.WithSkipGlazedCommandLayer()) + cobra.CheckErr(err) + + callCmd, err := NewCallToolCommand() + cobra.CheckErr(err) + + cobraCallCmd, err := cli.BuildCobraCommandFromWriterCommand(callCmd, cli.WithSkipGlazedCommandLayer()) + cobra.CheckErr(err) + + ToolsCmd.AddCommand(cobraListCmd) + ToolsCmd.AddCommand(cobraCallCmd) +} diff --git a/cmd/go-go-mcp/cmds/start.go b/cmd/go-go-mcp/cmds/start.go index 723fd91..b3844e4 100644 --- a/cmd/go-go-mcp/cmds/start.go +++ b/cmd/go-go-mcp/cmds/start.go @@ -10,25 +10,19 @@ import ( "time" "github.com/go-go-golems/glazed/pkg/cmds" - "github.com/go-go-golems/glazed/pkg/cmds/layers" + glazed_layers "github.com/go-go-golems/glazed/pkg/cmds/layers" + "github.com/go-go-golems/glazed/pkg/cmds/parameters" + "github.com/go-go-golems/go-go-mcp/cmd/go-go-mcp/cmds/server/layers" "github.com/go-go-golems/go-go-mcp/pkg/server" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" - - "github.com/go-go-golems/go-go-mcp/pkg/config" - config_provider "github.com/go-go-golems/go-go-mcp/pkg/tools/providers/config-provider" ) type StartCommandSettings struct { - Transport string `glazed.parameter:"transport"` - Port int `glazed.parameter:"port"` - Repositories []string `glazed.parameter:"repositories"` - Debug bool `glazed.parameter:"debug"` - TracingDir string `glazed.parameter:"tracing-dir"` - ConfigFile string `glazed.parameter:"config-file" help:"Path to the configuration file"` - Profile string `glazed.parameter:"profile" help:"Profile to use from the configuration file"` + Transport string `glazed.parameter:"transport"` + Port int `glazed.parameter:"port"` } type StartCommand struct { @@ -36,9 +30,9 @@ type StartCommand struct { } func NewStartCommand() (*StartCommand, error) { - defaultConfigFile, err := config.GetDefaultProfilesPath() + serverLayer, err := layers.NewServerParameterLayer() if err != nil { - return nil, errors.Wrap(err, "failed to get default profiles path") + return nil, errors.Wrap(err, "failed to create server parameter layer") } return &StartCommand{ @@ -63,87 +57,32 @@ Available transports: parameters.WithHelp("Port to listen on for SSE transport"), parameters.WithDefault(3001), ), - parameters.NewParameterDefinition( - "repositories", - parameters.ParameterTypeStringList, - parameters.WithHelp("List of directories containing shell command repositories"), - parameters.WithDefault([]string{}), - ), - parameters.NewParameterDefinition( - "debug", - parameters.ParameterTypeBool, - parameters.WithHelp("Enable debug mode for shell tool provider"), - parameters.WithDefault(false), - ), - parameters.NewParameterDefinition( - "tracing-dir", - parameters.ParameterTypeString, - parameters.WithHelp("Directory to store tool call traces"), - parameters.WithDefault(""), - ), - parameters.NewParameterDefinition( - "config-file", - parameters.ParameterTypeString, - parameters.WithHelp("Path to the configuration file"), - parameters.WithDefault(defaultConfigFile), - ), - parameters.NewParameterDefinition( - "profile", - parameters.ParameterTypeString, - parameters.WithHelp("Profile to use from the configuration file"), - parameters.WithDefault(""), - ), ), + cmds.WithLayersList(serverLayer), ), }, nil } func (c *StartCommand) Run( ctx context.Context, - parsedLayers *layers.ParsedLayers, + parsedLayers *glazed_layers.ParsedLayers, ) error { s := &StartCommandSettings{} - if err := parsedLayers.InitializeStruct(layers.DefaultSlug, s); err != nil { + if err := parsedLayers.InitializeStruct(glazed_layers.DefaultSlug, s); err != nil { return err } - // Create server - srv := server.NewServer(log.Logger) - - // Create tool provider options - toolProviderOptions := []config_provider.ConfigToolProviderOption{ - config_provider.WithDebug(s.Debug), - config_provider.WithWatch(true), - } - if s.TracingDir != "" { - toolProviderOptions = append(toolProviderOptions, config_provider.WithTracingDir(s.TracingDir)) - } - - var toolProvider *config_provider.ConfigToolProvider - var err error - - // Try to create tool provider from config file first - if s.ConfigFile != "" { - toolProvider, err = config_provider.CreateToolProviderFromConfig(s.ConfigFile, s.Profile, toolProviderOptions...) - if err != nil { - if !os.IsNotExist(err) || len(s.Repositories) == 0 { - fmt.Fprintf(os.Stderr, "Run 'go-go-mcp config init' to create a starting configuration file, and further edit it with 'go-go-mcp config edit'\n") - return errors.Wrap(err, "failed to create tool provider from config") - } - // Config file doesn't exist but we have repositories, continue with directories - } + serverSettings := &layers.ServerSettings{} + if err := parsedLayers.InitializeStruct(layers.ServerLayerSlug, serverSettings); err != nil { + return err } - // If no tool provider yet and we have repositories, create from directories - if toolProvider == nil && len(s.Repositories) > 0 { - toolProvider, err = config_provider.CreateToolProviderFromDirectories(s.Repositories, toolProviderOptions...) - if err != nil { - return errors.Wrap(err, "failed to create tool provider from directories") - } - } + // Create server + srv := server.NewServer(log.Logger) - if toolProvider == nil { - return fmt.Errorf("no valid configuration source found (neither config file nor repositories)") + toolProvider, err := layers.CreateToolProvider(serverSettings) + if err != nil { + return err } srv.GetRegistry().RegisterToolProvider(toolProvider) diff --git a/cmd/go-go-mcp/main.go b/cmd/go-go-mcp/main.go index e20b9ea..5d7a6f7 100644 --- a/cmd/go-go-mcp/main.go +++ b/cmd/go-go-mcp/main.go @@ -13,7 +13,8 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/alias" "github.com/go-go-golems/glazed/pkg/cmds/loaders" "github.com/go-go-golems/glazed/pkg/help" - server_cmds "github.com/go-go-golems/go-go-mcp/cmd/go-go-mcp/cmds" + mcp_cmds "github.com/go-go-golems/go-go-mcp/cmd/go-go-mcp/cmds" + server_cmds "github.com/go-go-golems/go-go-mcp/cmd/go-go-mcp/cmds/server" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -70,38 +71,40 @@ func initRootCmd() (*help.HelpSystem, error) { } // Initialize commands - rootCmd.AddCommand(server_cmds.ClientCmd) + rootCmd.AddCommand(mcp_cmds.ClientCmd) - err = server_cmds.InitClientCommand(helpSystem) + err = mcp_cmds.InitClientCommand(helpSystem) if err != nil { return nil, errors.Wrap(err, "could not initialize client command") } + rootCmd.AddCommand(server_cmds.ServerCmd) + rootCmd.AddCommand(runCommandCmd) // Create and add start command - startCmd, err := server_cmds.NewStartCommand() + startCmd, err := mcp_cmds.NewStartCommand() cobra.CheckErr(err) cobraStartCmd, err := cli.BuildCobraCommandFromBareCommand(startCmd, cli.WithSkipGlazedCommandLayer()) cobra.CheckErr(err) rootCmd.AddCommand(cobraStartCmd) // Create and add schema command - schemaCmd, err := server_cmds.NewSchemaCommand() + schemaCmd, err := mcp_cmds.NewSchemaCommand() cobra.CheckErr(err) cobraSchemaCmd, err := cli.BuildCobraCommandFromWriterCommand(schemaCmd, cli.WithSkipGlazedCommandLayer()) cobra.CheckErr(err) rootCmd.AddCommand(cobraSchemaCmd) - bridgeCmd := server_cmds.NewBridgeCommand(log.Logger) + bridgeCmd := mcp_cmds.NewBridgeCommand(log.Logger) rootCmd.AddCommand(bridgeCmd) // Add config command group - configCmd := server_cmds.NewConfigGroupCommand() + configCmd := mcp_cmds.NewConfigGroupCommand() rootCmd.AddCommand(configCmd) // Add Claude config command group - claudeConfigCmd := server_cmds.NewClaudeConfigCommand() + claudeConfigCmd := mcp_cmds.NewClaudeConfigCommand() rootCmd.AddCommand(claudeConfigCmd) return helpSystem, nil diff --git a/examples/bio-stuff/fetch-url.yaml b/examples/bio-stuff/fetch-url.yaml index e3713ab..3564c7d 100644 --- a/examples/bio-stuff/fetch-url.yaml +++ b/examples/bio-stuff/fetch-url.yaml @@ -1,5 +1,5 @@ name: fetch-url -short: Fetch and extract text content from URLs using lynx +short: Fetch and extract text content from URLs using lynx. foo. foo. Foo. long: | This command fetches and extracts plain text content from one or more URLs using the lynx text browser. It's particularly useful for automated content extraction, web scraping, and integration with LLM tools. diff --git a/go.mod b/go.mod index 224a8ac..794392d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.3 require ( github.com/JohannesKaufmann/html-to-markdown v1.6.0 - github.com/go-go-golems/clay v0.1.23 + github.com/go-go-golems/clay v0.1.25 github.com/go-go-golems/geppetto v0.4.34 github.com/go-go-golems/glazed v0.5.28 github.com/go-go-golems/parka v0.5.17 @@ -20,6 +20,7 @@ require ( github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + golang.org/x/sync v0.10.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -107,7 +108,6 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect diff --git a/go.sum b/go.sum index df9f9f4..f677ac6 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-go-golems/clay v0.1.23 h1:IPzxS1mTcU1GWjkjON5ed+mpjqhGVvXmn6M6cplpJr4= -github.com/go-go-golems/clay v0.1.23/go.mod h1:oS1t0/5pDbMpO+WqkKh94ddokbivW3dDX8Zqvy4JxYw= +github.com/go-go-golems/clay v0.1.25 h1:o7/rGaCmoHy9paue2t23N44ne8fcGOMsKLC1T5ZUios= +github.com/go-go-golems/clay v0.1.25/go.mod h1:oS1t0/5pDbMpO+WqkKh94ddokbivW3dDX8Zqvy4JxYw= github.com/go-go-golems/geppetto v0.4.34 h1:kVQqVqrXPPa+4tRPqAxTbvOAomM9XKd255nuUdBFCfE= github.com/go-go-golems/geppetto v0.4.34/go.mod h1:HGEsHKvH8HKH89CLWIcueYm46bue7LdFTtsFos3Uzyo= github.com/go-go-golems/glazed v0.5.28 h1:5ipOnzkhjfuxEP0LARNuAiXe2syPIPnywAvY98th6tk= From 793f5c330ceb5c0f7f4597e4aa9ba16d1a3e2dd3 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 12 Feb 2025 16:27:53 -0500 Subject: [PATCH 5/5] :arrow_up: :books: BUmp glazed and update documentation --- README.md | 44 +++++++++++++++ cmd/go-go-mcp/cmds/client/version.go | 25 --------- go.mod | 2 +- go.sum | 4 +- pkg/doc/topics/03-mcp-in-practice.md | 84 ++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 28 deletions(-) delete mode 100644 cmd/go-go-mcp/cmds/client/version.go diff --git a/README.md b/README.md index 65ed622..1dba535 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,50 @@ go-go-mcp start --transport stdio go-go-mcp start --transport sse --port 3001 ``` +The server automatically watches configured repositories and files for changes, reloading tools when: +- Files are added or removed from watched directories +- Tool configuration files are modified +- Repository structure changes + +This allows for dynamic tool updates without server restarts. + +#### Server Tools + +You can interact with tools directly without starting a server using the `server tools` commands: + +```bash +# List available tools using the 'all' profile +go-go-mcp server tools list --profile all + +# List only system monitoring tools +go-go-mcp server tools list --profile system + +# Call a tool directly +go-go-mcp server tools call system-monitor --args format=json,metrics=cpu,memory + +# Call a tool with JSON arguments +go-go-mcp server tools call calendar-event --json '{ + "title": "Team Meeting", + "start_time": "2024-02-01 10:00", + "duration": 60 +}' +``` + +The available tools depend on the selected profile: +```bash +# System monitoring profile +go-go-mcp server tools list --profile system +# Shows: system-monitor, disk-usage, etc. + +# Calendar profile +go-go-mcp server tools list --profile calendar +# Shows: calendar-event, calendar-availability, etc. + +# Data analysis profile +go-go-mcp server tools list --profile data +# Shows: data-analyzer, data-visualizer, etc. +``` + #### Client Mode Use the client subcommand to interact with an MCP server: diff --git a/cmd/go-go-mcp/cmds/client/version.go b/cmd/go-go-mcp/cmds/client/version.go deleted file mode 100644 index f797d9a..0000000 --- a/cmd/go-go-mcp/cmds/client/version.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// Version information -var ( - Version = "dev" - BuildTime = "unknown" - GitCommit = "none" -) - -// VersionCmd handles the "version" command -var VersionCmd = &cobra.Command{ - Use: "version", - Short: "Print version information", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("mcp-client version %s\n", Version) - fmt.Printf(" Build time: %s\n", BuildTime) - fmt.Printf(" Git commit: %s\n", GitCommit) - }, -} diff --git a/go.mod b/go.mod index 794392d..a04b224 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.3 require ( github.com/JohannesKaufmann/html-to-markdown v1.6.0 - github.com/go-go-golems/clay v0.1.25 + github.com/go-go-golems/clay v0.1.26 github.com/go-go-golems/geppetto v0.4.34 github.com/go-go-golems/glazed v0.5.28 github.com/go-go-golems/parka v0.5.17 diff --git a/go.sum b/go.sum index f677ac6..bc9c42e 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-go-golems/clay v0.1.25 h1:o7/rGaCmoHy9paue2t23N44ne8fcGOMsKLC1T5ZUios= -github.com/go-go-golems/clay v0.1.25/go.mod h1:oS1t0/5pDbMpO+WqkKh94ddokbivW3dDX8Zqvy4JxYw= +github.com/go-go-golems/clay v0.1.26 h1:JUw2QR4FIZ03zGBu/Ty/wc0ekZ8LWIm333+lx10CMVo= +github.com/go-go-golems/clay v0.1.26/go.mod h1:oS1t0/5pDbMpO+WqkKh94ddokbivW3dDX8Zqvy4JxYw= github.com/go-go-golems/geppetto v0.4.34 h1:kVQqVqrXPPa+4tRPqAxTbvOAomM9XKd255nuUdBFCfE= github.com/go-go-golems/geppetto v0.4.34/go.mod h1:HGEsHKvH8HKH89CLWIcueYm46bue7LdFTtsFos3Uzyo= github.com/go-go-golems/glazed v0.5.28 h1:5ipOnzkhjfuxEP0LARNuAiXe2syPIPnywAvY98th6tk= diff --git a/pkg/doc/topics/03-mcp-in-practice.md b/pkg/doc/topics/03-mcp-in-practice.md index 9f14dac..80b8ee1 100644 --- a/pkg/doc/topics/03-mcp-in-practice.md +++ b/pkg/doc/topics/03-mcp-in-practice.md @@ -673,6 +673,35 @@ Key concepts: Now we can start the MCP server with different tool sets based on our needs: +### File Watching + +The server automatically watches configured repositories and files for changes. This means you can: +- Add or remove tools while the server is running +- Modify tool configurations in real-time +- Update tool implementations without restarts + +File watching is enabled by default and can be controlled through the configuration: + +```yaml +profiles: + development: + tools: + directories: + - path: ./tools/system + watch: true # Enable watching for this directory + - path: ./tools/static + watch: false # Disable watching for static tools + files: + - path: ./tools/special-tool.yaml + watch: true # Watch individual files too +``` + +When changes are detected: +1. The server reloads affected tools +2. New tools become immediately available +3. Removed tools are unregistered +4. Modified tools are updated in-place + ### All Tools Start the server with all available tools: @@ -683,6 +712,13 @@ go-go-mcp start \ --profile all \ --transport sse \ --port 3001 + +# In another terminal, watch tools being loaded +tail -f go-go-mcp.log + +# Add a new tool while server is running +cp new-tool.yaml tools/system/ +# Watch the log to see it being loaded ``` ### System Monitoring @@ -697,6 +733,54 @@ go-go-mcp start \ --port 3001 ``` +### Direct Tool Interaction + +The `server tools` commands allow you to interact with tools directly without starting a server: + +```bash +# List tools in the system profile +go-go-mcp server tools list --profile system + +# Expected output: +# NAME DESCRIPTION +# system-monitor Monitor system resources and performance +# disk-usage Analyze disk space usage +# process-list List and filter running processes + +# Call system-monitor with JSON arguments +go-go-mcp server tools call system-monitor --json '{ + "format": "json", + "metrics": ["cpu", "memory", "disk"], + "watch": false +}' + +# Call disk-usage with key-value arguments +go-go-mcp server tools call disk-usage \ + --args directory=/home,unit=GB,type=*.log + +# Switch to calendar profile and list tools +go-go-mcp server tools list --profile calendar + +# Expected output: +# NAME DESCRIPTION +# calendar-event Manage calendar events and meetings +# calendar-availability Check calendar availability + +# Create a calendar event +go-go-mcp server tools call calendar-event --json '{ + "title": "Team Meeting", + "start_time": "2024-02-01 10:00", + "duration": 60, + "attendees": ["team@example.com"] +}' +``` + +This is particularly useful for: +- Testing tools during development +- Automation and scripting +- CI/CD pipelines +- Quick tool execution without server overhead + ### Data Analysis Start with data analysis tools: