Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion cmd/mcptools/commands/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"net/url"
"os"
"strings"
"time"

Expand All @@ -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) {
Expand Down Expand Up @@ -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}
// TODO: Handle headers for SSE servers
} else {
// Stdio-based server
args = append([]string{serverConfig.Command}, serverConfig.Args...)
}

// Apply environment variables
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 {
Expand Down
118 changes: 118 additions & 0 deletions cmd/mcptools/commands/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}