From a76c6913a51d6084b8b310e79df30621b34d3f62 Mon Sep 17 00:00:00 2001 From: ayakut16 Date: Tue, 1 Apr 2025 07:21:49 +0100 Subject: [PATCH 1/2] add server aliases functionality --- README.md | 22 +++++++ cmd/mcptools/main.go | 148 ++++++++++++++++++++++++++++++++++++++++++- pkg/alias/alias.go | 100 +++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 pkg/alias/alias.go diff --git a/README.md b/README.md index 52cab1a..8adc04e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Usage: mcp [command] Available Commands: + alias Manage MCP server aliases call Call a tool, resource, or prompt on the MCP server help Help about any command mock Create a mock MCP server with tools, prompts, and resources @@ -270,6 +271,27 @@ Special Commands: /q, /quit, exit Exit the shell ``` +## Server Aliases + +MCP Tools allows you to save and reuse server commands with friendly aliases: + +```bash +# Add a new server alias +mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/ + +# List all registered server aliases +mcp alias list + +# Remove a server alias +mcp alias remove myfs + +# Use an alias with any MCP command +mcp tools myfs +mcp call read_file --params '{"path": "README.md"}' myfs +``` + +Server aliases are stored in `$HOME/.mcpt/aliases.json` and provide a convenient way to work with commonly used MCP servers without typing long commands repeatedly. + ## Server Modes MCP Tools can operate as both a client and a server, with two server modes available: diff --git a/cmd/mcptools/main.go b/cmd/mcptools/main.go index 0ff8918..b235876 100644 --- a/cmd/mcptools/main.go +++ b/cmd/mcptools/main.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/f/mcptools/pkg/alias" "github.com/f/mcptools/pkg/client" "github.com/f/mcptools/pkg/jsonutils" "github.com/f/mcptools/pkg/mock" @@ -56,7 +57,23 @@ var createClientFunc = func(args []string) (*client.Client, error) { return nil, errCommandRequired } - if len(args) == 1 && (strings.HasPrefix(args[0], "http://") || strings.HasPrefix(args[0], "https://")) { + isHTTP := func(str string) bool { + return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://") + } + + // Check if the first argument is an alias + if len(args) == 1 { + server, found := alias.GetServerCommand(args[0]) + if found { + if isHTTP(server) { + return client.NewHTTP(server), nil + } + cmdParts := client.ParseCommandString(server) + return client.NewStdio(cmdParts), nil + } + } + + if len(args) == 1 && isHTTP(args[0]) { return client.NewHTTP(args[0]), nil } @@ -78,6 +95,7 @@ func main() { newShellCmd(), newMockCmd(), proxyCmd(), + aliasCmd(), ) if err := rootCmd.Execute(); err != nil { @@ -1162,3 +1180,131 @@ Example: return cmd } + +func aliasCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "alias", + Short: "Manage MCP server aliases", + Long: `Manage aliases for MCP servers. + +This command allows you to register MCP server commands with a friendly name and +reuse them later. + +Aliases are stored in $HOME/.mcpt/aliases.json. + +Examples: + # Add a new server alias + mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/ + + # List all registered server aliases + mcp alias list + + # Remove a server alias + mcp alias remove myfs + + # Use an alias with any MCP command + mcp tools myfs`, + } + + cmd.AddCommand(aliasAddCmd()) + cmd.AddCommand(aliasListCmd()) + cmd.AddCommand(aliasRemoveCmd()) + + return cmd +} + +func aliasAddCmd() *cobra.Command { + addCmd := &cobra.Command{ + Use: "add [alias] [command args...]", + Short: "Add a new MCP server alias", + DisableFlagParsing: true, + Long: `Add a new alias for an MCP server command. + +The alias will be registered and can be used in place of the server command. + +Example: + mcp alias add myfs npx -y @modelcontextprotocol/server-filesystem ~/`, + Args: cobra.MinimumNArgs(2), + RunE: func(thisCmd *cobra.Command, args []string) error { + if len(args) == 1 && (args[0] == flagHelp || args[0] == flagHelpShort) { + _ = thisCmd.Help() + return nil + } + + aliasName := args[0] + serverCommand := strings.Join(args[1:], " ") + + aliases, err := alias.Load() + if err != nil { + return fmt.Errorf("error loading aliases: %w", err) + } + + aliases[aliasName] = alias.ServerAlias{ + Command: serverCommand, + } + + if saveErr := alias.Save(aliases); saveErr != nil { + return fmt.Errorf("error saving aliases: %w", saveErr) + } + + fmt.Printf("Alias '%s' registered for command: %s\n", aliasName, serverCommand) + return nil + }, + } + return addCmd +} + +func aliasListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all registered MCP server aliases", + RunE: func(_ *cobra.Command, _ []string) error { + // Load existing aliases + aliases, err := alias.Load() + if err != nil { + return fmt.Errorf("error loading aliases: %w", err) + } + + if len(aliases) == 0 { + fmt.Println("No aliases registered.") + return nil + } + + fmt.Println("Registered MCP server aliases:") + for name, a := range aliases { + fmt.Printf(" %s: %s\n", name, a.Command) + } + + return nil + }, + } +} + +func aliasRemoveCmd() *cobra.Command { + return &cobra.Command{ + Use: "remove [alias]", + Short: "Remove an MCP server alias", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + aliasName := args[0] + + aliases, err := alias.Load() + if err != nil { + return fmt.Errorf("error loading aliases: %w", err) + } + + if _, exists := aliases[aliasName]; !exists { + return fmt.Errorf("alias '%s' not found", aliasName) + } + + delete(aliases, aliasName) + + if saveErr := alias.Save(aliases); saveErr != nil { + return fmt.Errorf("error saving aliases: %w", saveErr) + } + + fmt.Printf("Alias '%s' removed.\n", aliasName) + return nil + }, + } +} diff --git a/pkg/alias/alias.go b/pkg/alias/alias.go new file mode 100644 index 0000000..c9baaf5 --- /dev/null +++ b/pkg/alias/alias.go @@ -0,0 +1,100 @@ +/* +Package alias implements server alias functionality for MCP. +*/ +package alias + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +// ServerAlias represents a single server command alias. +type ServerAlias struct { + Command string `json:"command"` +} + +// Aliases stores command aliases for MCP servers. +type Aliases map[string]ServerAlias + +// GetConfigPath returns the path to the aliases configuration file. +func GetConfigPath() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %w", err) + } + + configDir := filepath.Join(homeDir, ".mcpt") + mkdirErr := os.MkdirAll(configDir, 0o750) + if mkdirErr != nil { + return "", fmt.Errorf("failed to create config directory: %w", mkdirErr) + } + + return filepath.Join(configDir, "aliases.json"), nil +} + +// Load loads server aliases from the configuration file. +func Load() (Aliases, error) { + configPath, err := GetConfigPath() + if err != nil { + return nil, err + } + + aliases := make(Aliases) + + var statErr error + if _, statErr = os.Stat(configPath); os.IsNotExist(statErr) { + return aliases, nil + } + + configFile, err := os.ReadFile(configPath) // #nosec G304 - configPath is generated internally by GetConfigPath + if err != nil { + return nil, fmt.Errorf("failed to read alias config file: %w", err) + } + + if len(configFile) == 0 { + return aliases, nil + } + + if unmarshalErr := json.Unmarshal(configFile, &aliases); unmarshalErr != nil { + return nil, fmt.Errorf("failed to parse alias config file: %w", unmarshalErr) + } + + return aliases, nil +} + +// Save saves server aliases to the configuration file. +func Save(aliases Aliases) error { + configPath, err := GetConfigPath() + if err != nil { + return err + } + + configJSON, err := json.MarshalIndent(aliases, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal alias config: %w", err) + } + + writeErr := os.WriteFile(configPath, configJSON, 0o600) // #nosec G304 - configPath is generated internally by GetConfigPath + if writeErr != nil { + return fmt.Errorf("failed to write alias config file: %w", writeErr) + } + + return nil +} + +// GetServerCommand retrieves the server command for a given alias. +func GetServerCommand(aliasName string) (string, bool) { + aliases, err := Load() + if err != nil { + return "", false + } + + alias, exists := aliases[aliasName] + if !exists { + return "", false + } + + return alias.Command, true +} From a3ac69c414c5e115c322ab3b60ae31761b9fcf1c Mon Sep 17 00:00:00 2001 From: ayakut16 Date: Wed, 2 Apr 2025 22:22:15 +0100 Subject: [PATCH 2/2] refactor json_utils and main_test.go to get rid of lint errors. --- cmd/mcptools/main_test.go | 16 +- pkg/jsonutils/jsonutils.go | 304 ++++++++++++++++++-------------- pkg/jsonutils/jsonutils_test.go | 9 +- 3 files changed, 178 insertions(+), 151 deletions(-) diff --git a/cmd/mcptools/main_test.go b/cmd/mcptools/main_test.go index 129deed..81847d7 100644 --- a/cmd/mcptools/main_test.go +++ b/cmd/mcptools/main_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - "github.com/f/mcptools/pkg/client" "github.com/f/mcptools/pkg/transport" ) @@ -323,7 +322,9 @@ func TestExecuteShell(t *testing.T) { func TestProxyToolRegistration(t *testing.T) { // Create a temporary directory for test files tmpDir := t.TempDir() - os.Setenv("HOME", tmpDir) + if err := os.Setenv("HOME", tmpDir); err != nil { + t.Fatalf("Failed to set HOME environment variable: %v", err) + } // Test cases testCases := []struct { @@ -397,7 +398,9 @@ func TestProxyToolRegistration(t *testing.T) { func TestProxyToolUnregistration(t *testing.T) { // Create a temporary directory for test files tmpDir := t.TempDir() - os.Setenv("HOME", tmpDir) + if err := os.Setenv("HOME", tmpDir); err != nil { + t.Fatalf("Failed to set HOME environment variable: %v", err) + } // First register a tool cmd := proxyToolCmd() @@ -413,7 +416,9 @@ func TestProxyToolUnregistration(t *testing.T) { } // Now try to unregister it - cmd.Flags().Set("unregister", "true") + if setErr := cmd.Flags().Set("unregister", "true"); setErr != nil { + t.Fatalf("Failed to set unregister flag: %v", setErr) + } err = cmd.RunE(cmd, []string{"test_tool"}) if err != nil { t.Errorf("Error unregistering tool: %v", err) @@ -430,9 +435,6 @@ func TestProxyToolUnregistration(t *testing.T) { } } -// testCreateClient is a test-specific version of createClient that always returns a client with our mock transport -var testCreateClient func(args []string) (*client.Client, error) - func TestShellCommands(t *testing.T) { // Create a mock server for testing mockServer := NewMockTransport() diff --git a/pkg/jsonutils/jsonutils.go b/pkg/jsonutils/jsonutils.go index b8d37d0..dc9d80c 100644 --- a/pkg/jsonutils/jsonutils.go +++ b/pkg/jsonutils/jsonutils.go @@ -16,7 +16,7 @@ import ( "golang.org/x/term" ) -// ANSI color codes for terminal output +// ANSI color codes for terminal output. const ( ColorReset = "\033[0m" ColorBold = "\033[1m" @@ -29,7 +29,15 @@ const ( ColorGray = "\033[37m" ) -// isTerminal determines if stdout is a terminal (for colorized output) +const ( + typeObject = "object" + typeArray = "array" + typeAny = "any" + shortTypeObject = "obj" + shortTypeArray = "arr" +) + +// isTerminal determines if stdout is a terminal (for colorized output). func isTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) } @@ -208,7 +216,7 @@ func formatToolsList(tools any) (string, error) { return buf.String(), nil } -// formatToolNameWithParams formats a tool name with parameters, adding colors if enabled +// formatToolNameWithParams formats a tool name with parameters, adding colors if enabled. func formatToolNameWithParams(name, params string, useColors bool) string { if !useColors { return fmt.Sprintf("%s(%s)", name, params) @@ -230,7 +238,7 @@ func formatToolNameWithParams(name, params string, useColors bool) string { ColorGreen, coloredParams, ColorReset) } -// Shortens type names for display +// Shortens type names for display. func shortenTypeName(typeName string) string { switch typeName { case "string": @@ -241,8 +249,8 @@ func shortenTypeName(typeName string) string { return "bool" case "number": return "num" - case "object": - return "obj" + case typeObject: + return shortTypeObject default: // If it's already 3 letters or less, return as is if len(typeName) <= 3 { @@ -253,13 +261,13 @@ func shortenTypeName(typeName string) string { } } -// formatObjectProperties formats object properties recursively -func formatObjectProperties(propMap map[string]any, requiredProps []string) string { +// formatObjectProperties formats object properties recursively. +func formatObjectProperties(propMap map[string]any) string { if len(propMap) == 0 { - return "obj" + return shortTypeObject } - // Get all property names and sort them for consistent output + // Get all property names and sort them for consistent output. var propNames []string for name := range propMap { propNames = append(propNames, name) @@ -276,48 +284,49 @@ func formatObjectProperties(propMap map[string]any, requiredProps []string) stri propType, _ := propDef["type"].(string) // Handle nested objects - if propType == "object" { + switch propType { + case typeObject: var nestedRequired []string if req, hasReq := propDef["required"]; hasReq && req != nil { - if reqArray, ok := req.([]any); ok { + if reqArray, isReqArray := req.([]any); isReqArray { for _, r := range reqArray { - if reqStr, ok := r.(string); ok { - nestedRequired = append(nestedRequired, reqStr) + if reqStr, isReqStr := r.(string); isReqStr { + _ = append(nestedRequired, reqStr) } } } } if properties, hasProps := propDef["properties"]; hasProps && properties != nil { - if propsMap, ok := properties.(map[string]any); ok { - propType = formatObjectProperties(propsMap, nestedRequired) + if propsMap, isPropsMap := properties.(map[string]any); isPropsMap { + propType = formatObjectProperties(propsMap) } } else { - propType = "obj" + propType = shortTypeObject } - } else if propType == "array" { + case typeArray: // Handle array types if items, hasItems := propDef["items"]; hasItems && items != nil { - if itemsMap, ok := items.(map[string]any); ok { + if itemsMap, isItemsMap := items.(map[string]any); isItemsMap { itemType, hasType := itemsMap["type"] if hasType { - if itemTypeStr, ok := itemType.(string); ok { - if itemTypeStr == "object" { + if itemTypeStr, isItemTypeStr := itemType.(string); isItemTypeStr { + if itemTypeStr == typeObject { // Handle array of objects var nestedRequired []string if req, hasReq := itemsMap["required"]; hasReq && req != nil { - if reqArray, ok := req.([]any); ok { + if reqArray, isReqArray := req.([]any); isReqArray { for _, r := range reqArray { - if reqStr, ok := r.(string); ok { - nestedRequired = append(nestedRequired, reqStr) + if reqStr, isReqStr := r.(string); isReqStr { + _ = append(nestedRequired, reqStr) } } } } if properties, hasProps := itemsMap["properties"]; hasProps && properties != nil { - if propsMap, ok := properties.(map[string]any); ok { - propType = formatObjectProperties(propsMap, nestedRequired) + "[]" + if propsMap, isPropsMap := properties.(map[string]any); isPropsMap { + propType = formatObjectProperties(propsMap) + "[]" } } else { propType = "obj[]" @@ -332,7 +341,8 @@ func formatObjectProperties(propMap map[string]any, requiredProps []string) stri } else { propType = "arr" } - } else { + + default: // Regular types propType = shortenTypeName(propType) } @@ -343,142 +353,164 @@ func formatObjectProperties(propMap map[string]any, requiredProps []string) stri return "{" + strings.Join(props, ",") + "}" } -// formatParameters formats the parameters for display in the tool name +// formatParameters formats the parameters for display in the tool name. func formatParameters(params any) string { // Handle case where we have an inputSchema structure if inputSchema, ok := params.(map[string]any); ok { // Check if this is the JSON Schema structure - if properties, hasProps := inputSchema["properties"]; hasProps && properties != nil { - propsMap, ok := properties.(map[string]any) - if !ok { - return "" + if inputProps, hasInputProps := inputSchema["properties"]; hasInputProps && inputProps != nil { + return formatInputSchema(inputSchema) + } + } + + // Call the legacy format handler for other parameter formats + return formatLegacyParameters(params) +} + +// formatInputSchema formats parameters from a JSON Schema structure. +func formatInputSchema(inputSchema map[string]any) string { + inputProps := inputSchema["properties"] + inputPropsMap, isInputPropsMap := inputProps.(map[string]any) + if !isInputPropsMap { + return "" + } + + // Get required parameters + var requiredParams []string + if required, hasRequired := inputSchema["required"]; hasRequired && required != nil { + if reqArray, isReqArray := required.([]any); isReqArray { + for _, r := range reqArray { + if reqStr, isReqStr := r.(string); isReqStr { + requiredParams = append(requiredParams, reqStr) + } } + } + } - // Get required parameters - var requiredParams []string - if required, hasRequired := inputSchema["required"]; hasRequired && required != nil { - if reqArray, ok := required.([]any); ok { + // Get all parameter names and sort them for consistent output + var paramNames []string + for name := range inputPropsMap { + paramNames = append(paramNames, name) + } + + // Sort parameter names + sort.Strings(paramNames) + + // Format parameters, putting required ones first + var requiredParamStrs []string + var optionalParamStrs []string + + for _, paramName := range paramNames { + propDef := inputPropsMap[paramName] + propDefMap, isPropDefMap := propDef.(map[string]any) + if !isPropDefMap { + continue + } + + paramType, _ := propDefMap["type"].(string) + + // Handle object types + switch paramType { + case "object": + // Get nested required fields + var nestedRequired []string + if req, hasReq := propDefMap["required"]; hasReq && req != nil { + if reqArray, isReqArray := req.([]any); isReqArray { for _, r := range reqArray { - if reqStr, ok := r.(string); ok { - requiredParams = append(requiredParams, reqStr) + if reqStr, isReqStr := r.(string); isReqStr { + _ = append(nestedRequired, reqStr) } } } } - // Get all parameter names and sort them for consistent output - var paramNames []string - for name := range propsMap { - paramNames = append(paramNames, name) + // Format object properties + if properties, hasProps := propDefMap["properties"]; hasProps && properties != nil { + if propsMap, isPropsMap := properties.(map[string]any); isPropsMap { + paramType = formatObjectProperties(propsMap) + } + } else { + paramType = "obj" + } + case "array": + paramType = formatArrayType(propDefMap) + default: + paramType = shortenTypeName(paramType) + } + + // Check if this parameter is required + isRequired := false + for _, req := range requiredParams { + if req == paramName { + isRequired = true + break } + } + + if isRequired { + requiredParamStrs = append(requiredParamStrs, fmt.Sprintf("%s:%s", paramName, paramType)) + } else { + optionalParamStrs = append(optionalParamStrs, fmt.Sprintf("[%s:%s]", paramName, paramType)) + } + } - // Sort parameter names - sort.Strings(paramNames) + // Join all parameters, required first, then optional + var allParamStrs []string + allParamStrs = append(allParamStrs, requiredParamStrs...) + allParamStrs = append(allParamStrs, optionalParamStrs...) - // Format parameters, putting required ones first - var requiredParamStrs []string - var optionalParamStrs []string + return strings.Join(allParamStrs, ", ") +} - for _, paramName := range paramNames { - propDef, _ := propsMap[paramName] - propDefMap, ok := propDef.(map[string]any) - if !ok { - continue - } +// formatArrayType handles the formatting of array type parameters. +func formatArrayType(propDefMap map[string]any) string { + items, hasItems := propDefMap["items"] + if !hasItems || items == nil { + return shortTypeArray + } - paramType, _ := propDefMap["type"].(string) - - // Handle object types - if paramType == "object" { - // Get nested required fields - var nestedRequired []string - if req, hasReq := propDefMap["required"]; hasReq && req != nil { - if reqArray, ok := req.([]any); ok { - for _, r := range reqArray { - if reqStr, ok := r.(string); ok { - nestedRequired = append(nestedRequired, reqStr) - } - } - } - } + itemsMap, isItemsMap := items.(map[string]any) + if !isItemsMap { + return shortTypeArray + } - // Format object properties - if properties, hasProps := propDefMap["properties"]; hasProps && properties != nil { - if propsMap, ok := properties.(map[string]any); ok { - paramType = formatObjectProperties(propsMap, nestedRequired) - } - } else { - paramType = "obj" - } - } else if paramType == "array" { - // Handle array types - if items, hasItems := propDefMap["items"]; hasItems && items != nil { - if itemsMap, ok := items.(map[string]any); ok { - itemType, hasType := itemsMap["type"] - if hasType { - if itemTypeStr, ok := itemType.(string); ok { - if itemTypeStr == "object" { - // Handle array of objects - var nestedRequired []string - if req, hasReq := itemsMap["required"]; hasReq && req != nil { - if reqArray, ok := req.([]any); ok { - for _, r := range reqArray { - if reqStr, ok := r.(string); ok { - nestedRequired = append(nestedRequired, reqStr) - } - } - } - } + itemType, hasType := itemsMap["type"] + if !hasType { + return shortTypeArray + } - if properties, hasProps := itemsMap["properties"]; hasProps && properties != nil { - if propsMap, ok := properties.(map[string]any); ok { - paramType = formatObjectProperties(propsMap, nestedRequired) + "[]" - } - } else { - paramType = "obj[]" - } - } else { - // Simple array - paramType = shortenTypeName(itemTypeStr) + "[]" - } - } - } - } - } else { - // If no item type is specified, just use "array" - paramType = "arr" - } - } else { - // Shorten non-array type names - paramType = shortenTypeName(paramType) - } + itemTypeStr, isItemTypeStr := itemType.(string) + if !isItemTypeStr { + return shortTypeArray + } - // Check if this parameter is required - isRequired := false - for _, req := range requiredParams { - if req == paramName { - isRequired = true - break + if itemTypeStr == "object" { + // Handle array of objects + var nestedRequired []string + if req, hasReq := itemsMap["required"]; hasReq && req != nil { + if reqArray, isReqArray := req.([]any); isReqArray { + for _, r := range reqArray { + if reqStr, isReqStr := r.(string); isReqStr { + _ = append(nestedRequired, reqStr) } } - - if isRequired { - requiredParamStrs = append(requiredParamStrs, fmt.Sprintf("%s:%s", paramName, paramType)) - } else { - optionalParamStrs = append(optionalParamStrs, fmt.Sprintf("[%s:%s]", paramName, paramType)) - } } + } - // Join all parameters, required first, then optional - var allParamStrs []string - allParamStrs = append(allParamStrs, requiredParamStrs...) - allParamStrs = append(allParamStrs, optionalParamStrs...) - - return strings.Join(allParamStrs, ", ") + if properties, hasProps := itemsMap["properties"]; hasProps && properties != nil { + if propsMap, isPropsMap := properties.(map[string]any); isPropsMap { + return formatObjectProperties(propsMap) + "[]" + } } + return "obj[]" } - // Original function for other parameter formats + // Simple array + return shortenTypeName(itemTypeStr) + "[]" +} + +// formatLegacyParameters handles the legacy parameter formats. +func formatLegacyParameters(params any) string { switch p := params.(type) { case string: // If parameters is already a string (e.g., "param1:type1,param2:type2") diff --git a/pkg/jsonutils/jsonutils_test.go b/pkg/jsonutils/jsonutils_test.go index 1e8a6a3..e3a05e1 100644 --- a/pkg/jsonutils/jsonutils_test.go +++ b/pkg/jsonutils/jsonutils_test.go @@ -450,14 +450,7 @@ func containsSubstring(s, substr string) bool { return strings.Contains(s, substr) } -// Checks if a string has a specified type name (accounting for both full and shortened versions) -func hasTypeName(s, typeName string) bool { - fullPattern := typeName + "\"" - shortPattern := shortenTypeName(typeName) + "\"" - return strings.Contains(s, fullPattern) || strings.Contains(s, shortPattern) -} - -// TestNormalizeParameterType tests the type name normalization functionality +// TestNormalizeParameterType tests the type name normalization functionality. func TestNormalizeParameterType(t *testing.T) { testCases := []struct { input string