From 21fd12d37edb1532d6dade4454235697defaf249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Ara=C3=BAjo?= Date: Sun, 4 Jan 2026 21:54:46 -0300 Subject: [PATCH] feat: add config source prefix syntax for loading servers from IDE configs Add support for running MCP servers directly from IDE/editor configs using a prefix syntax like 'claude-code:firecrawl' or 'cursor:myserver'. This loads the full server config (command, args, env vars) from the specified config source and runs the server with proper environment. Supported prefixes: - claude-code (from ~/.claude.json) - claude-desktop (from Claude Desktop config) - cursor (from Cursor settings) - vscode / vscode-insiders - windsurf Example usage: mcp tools claude-code:firecrawl mcp call claude-code:firecrawl firecrawl_search --params '{"query": "test"}' Changes: - Add ParseConfigPrefix() to parse 'source:server' format - Add GetServerFromConfigSource() to load server config from IDE configs - Update CreateClientFunc to handle prefix syntax before alias lookup - Add comprehensive unit tests for prefix parsing --- cmd/mcptools/commands/utils.go | 100 ++++++++++++++++++++++- cmd/mcptools/commands/utils_test.go | 118 ++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go index 3789004..98a9e50 100644 --- a/cmd/mcptools/commands/utils.go +++ b/cmd/mcptools/commands/utils.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "net/url" + "os" "strings" "time" @@ -29,6 +30,77 @@ func IsHTTP(str string) bool { return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://") || strings.HasPrefix(str, "localhost:") } +// validConfigPrefixes defines the valid config source prefixes. +var validConfigPrefixes = map[string]bool{ + "vscode": true, + "vscode-insiders": true, + "windsurf": true, + "cursor": true, + "claude-desktop": true, + "claude-code": true, +} + +// ParseConfigPrefix parses a config source prefix from a server specification. +// Examples: +// +// "claude-code:firecrawl" -> ("claude-code", "firecrawl", true) +// "cursor:myserver" -> ("cursor", "myserver", true) +// "myalias" -> ("", "myalias", false) +func ParseConfigPrefix(serverSpec string) (configSource, serverName string, hasPrefix bool) { + if idx := strings.Index(serverSpec, ":"); idx > 0 { + prefix := serverSpec[:idx] + if validConfigPrefixes[strings.ToLower(prefix)] { + return strings.ToLower(prefix), serverSpec[idx+1:], true + } + } + return "", serverSpec, false +} + +// GetServerFromConfigSource loads a server configuration from a config source. +// Returns the server config or an error if not found. +func GetServerFromConfigSource(configSource, serverName string) (*ServerConfig, error) { + // Load the configs file to get the alias path + configs, err := loadConfigsFile() + if err != nil { + return nil, fmt.Errorf("failed to load configs: %w", err) + } + + // Get the config alias + aliasConfig, ok := configs.Aliases[configSource] + if !ok { + return nil, fmt.Errorf("config source '%s' not found", configSource) + } + + // Expand and read the config file + configPath := expandPath(aliasConfig.Path) + + // Get servers using existing function based on JSONPath + var servers []ServerConfig + var scanErr error + + if strings.Contains(aliasConfig.JSONPath, "mcp.servers") { + servers, scanErr = scanVSCodeConfig(configPath, aliasConfig.Source) + } else { + servers, scanErr = scanMCPServersConfig(configPath, aliasConfig.Source) + } + + if scanErr != nil { + return nil, fmt.Errorf("failed to scan config: %w", scanErr) + } + + // Find the specific server + var available []string + for _, server := range servers { + if server.Name == serverName { + return &server, nil + } + available = append(available, server.Name) + } + + return nil, fmt.Errorf("server '%s' not found in %s\nAvailable servers: %s", + serverName, configSource, strings.Join(available, ", ")) +} + // buildAuthHeader builds an Authorization header from the available auth options. // It returns the header value and a cleaned URL (with embedded credentials removed). func buildAuthHeader(originalURL string) (string, string, error) { @@ -92,7 +164,33 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl return nil, ErrCommandRequired } - // Check if the first argument is an alias + // Check for config source prefix (e.g., "claude-code:firecrawl") + if len(args) == 1 { + configSource, serverName, hasPrefix := ParseConfigPrefix(args[0]) + if hasPrefix { + serverConfig, err := GetServerFromConfigSource(configSource, serverName) + if err != nil { + return nil, err + } + + // Build args from server config + if serverConfig.URL != "" { + // URL-based server (SSE/HTTP) + args = []string{serverConfig.URL} + // Note: Headers for SSE servers are not yet supported + } else { + // Stdio-based server + args = append([]string{serverConfig.Command}, serverConfig.Args...) + } + + // Apply environment variables (errors are ignored as setenv rarely fails) + for key, value := range serverConfig.Env { + _ = os.Setenv(key, value) + } + } + } + + // Check if the first argument is an alias (fallback if no prefix match) if len(args) == 1 { server, found := alias.GetServerCommand(args[0]) if found { diff --git a/cmd/mcptools/commands/utils_test.go b/cmd/mcptools/commands/utils_test.go index 0eb06b8..f164536 100644 --- a/cmd/mcptools/commands/utils_test.go +++ b/cmd/mcptools/commands/utils_test.go @@ -190,3 +190,121 @@ nested {"key":"value"}`[1:] // remove first newline }) } } + +func TestParseConfigPrefix(t *testing.T) { + tests := []struct { + name string + serverSpec string + wantConfigSource string + wantServerName string + wantHasPrefix bool + }{ + { + name: "claude-code prefix", + serverSpec: "claude-code:firecrawl", + wantConfigSource: "claude-code", + wantServerName: "firecrawl", + wantHasPrefix: true, + }, + { + name: "claude-desktop prefix", + serverSpec: "claude-desktop:playwright", + wantConfigSource: "claude-desktop", + wantServerName: "playwright", + wantHasPrefix: true, + }, + { + name: "cursor prefix", + serverSpec: "cursor:myserver", + wantConfigSource: "cursor", + wantServerName: "myserver", + wantHasPrefix: true, + }, + { + name: "vscode prefix", + serverSpec: "vscode:server", + wantConfigSource: "vscode", + wantServerName: "server", + wantHasPrefix: true, + }, + { + name: "vscode-insiders prefix", + serverSpec: "vscode-insiders:server", + wantConfigSource: "vscode-insiders", + wantServerName: "server", + wantHasPrefix: true, + }, + { + name: "windsurf prefix", + serverSpec: "windsurf:server", + wantConfigSource: "windsurf", + wantServerName: "server", + wantHasPrefix: true, + }, + { + name: "case insensitive prefix", + serverSpec: "Claude-Code:firecrawl", + wantConfigSource: "claude-code", + wantServerName: "firecrawl", + wantHasPrefix: true, + }, + { + name: "no prefix - simple alias", + serverSpec: "myalias", + wantConfigSource: "", + wantServerName: "myalias", + wantHasPrefix: false, + }, + { + name: "unknown prefix treated as no prefix", + serverSpec: "unknown:server", + wantConfigSource: "", + wantServerName: "unknown:server", + wantHasPrefix: false, + }, + { + name: "HTTP URL not treated as prefix", + serverSpec: "http://localhost:8080", + wantConfigSource: "", + wantServerName: "http://localhost:8080", + wantHasPrefix: false, + }, + { + name: "HTTPS URL not treated as prefix", + serverSpec: "https://example.com", + wantConfigSource: "", + wantServerName: "https://example.com", + wantHasPrefix: false, + }, + { + name: "server name with colons", + serverSpec: "claude-code:server:with:colons", + wantConfigSource: "claude-code", + wantServerName: "server:with:colons", + wantHasPrefix: true, + }, + { + name: "empty string", + serverSpec: "", + wantConfigSource: "", + wantServerName: "", + wantHasPrefix: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configSource, serverName, hasPrefix := ParseConfigPrefix(tt.serverSpec) + + if configSource != tt.wantConfigSource { + t.Errorf("ParseConfigPrefix() configSource = %q, want %q", configSource, tt.wantConfigSource) + } + if serverName != tt.wantServerName { + t.Errorf("ParseConfigPrefix() serverName = %q, want %q", serverName, tt.wantServerName) + } + if hasPrefix != tt.wantHasPrefix { + t.Errorf("ParseConfigPrefix() hasPrefix = %v, want %v", hasPrefix, tt.wantHasPrefix) + } + }) + } +}