diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index dfd66d288..0734edf54 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -217,6 +217,73 @@ func formatToolsetName(name string) string { } } +// extractPropertyType extracts the type string from a property map. +// It handles array types by looking up the item type. +func extractPropertyType(propMap map[string]interface{}) string { + typeVal, ok := propMap["type"].(string) + if !ok { + return "unknown" + } + + if typeVal != "array" { + return typeVal + } + + return extractArrayType(propMap) +} + +// extractArrayType extracts the array item type from a property map. +func extractArrayType(propMap map[string]interface{}) string { + items, ok := propMap["items"].(map[string]interface{}) + if !ok { + return "array" + } + + itemType, ok := items["type"].(string) + if !ok { + return "array" + } + + return itemType + "[]" +} + +// extractPropertyDescription extracts the description from a property map. +func extractPropertyDescription(propMap map[string]interface{}) string { + desc, ok := propMap["description"].(string) + if !ok { + return "" + } + return desc +} + +// formatParamLine formats a single parameter documentation line. +func formatParamLine(propName string, prop interface{}, required []string) string { + requiredStr := "optional" + if contains(required, propName) { + requiredStr = "required" + } + + typeStr := "unknown" + description := "" + + if propMap, ok := prop.(map[string]interface{}); ok { + typeStr = extractPropertyType(propMap) + description = extractPropertyDescription(propMap) + } + + return fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr) +} + +// getSortedParamNames returns sorted parameter names from schema properties. +func getSortedParamNames(properties map[string]interface{}) []string { + paramNames := make([]string, 0, len(properties)) + for propName := range properties { + paramNames = append(paramNames, propName) + } + sort.Strings(paramNames) + return paramNames +} + func generateToolDoc(tool mcp.Tool) string { var lines []string @@ -225,51 +292,15 @@ func generateToolDoc(tool mcp.Tool) string { // Parameters schema := tool.InputSchema - if len(schema.Properties) > 0 { - // Get parameter names and sort them for deterministic order - var paramNames []string - for propName := range schema.Properties { - paramNames = append(paramNames, propName) - } - sort.Strings(paramNames) - - for _, propName := range paramNames { - prop := schema.Properties[propName] - required := contains(schema.Required, propName) - requiredStr := "optional" - if required { - requiredStr = "required" - } - - // Get the type and description - typeStr := "unknown" - description := "" - - if propMap, ok := prop.(map[string]interface{}); ok { - if typeVal, ok := propMap["type"].(string); ok { - if typeVal == "array" { - if items, ok := propMap["items"].(map[string]interface{}); ok { - if itemType, ok := items["type"].(string); ok { - typeStr = itemType + "[]" - } - } else { - typeStr = "array" - } - } else { - typeStr = typeVal - } - } - - if desc, ok := propMap["description"].(string); ok { - description = desc - } - } - - paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr) - lines = append(lines, paramLine) - } - } else { + if len(schema.Properties) == 0 { lines = append(lines, " - No parameters required") + return strings.Join(lines, "\n") + } + + paramNames := getSortedParamNames(schema.Properties) + for _, propName := range paramNames { + paramLine := formatParamLine(propName, schema.Properties[propName], schema.Required) + lines = append(lines, paramLine) } return strings.Join(lines, "\n") diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index cad002666..0955372da 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -18,6 +18,9 @@ var version = "version" var commit = "commit" var date = "date" +// Configuration key constants +const readOnlyKey = "read-only" + var ( rootCmd = &cobra.Command{ Use: "server", @@ -51,7 +54,7 @@ var ( Token: token, EnabledToolsets: enabledToolsets, DynamicToolsets: viper.GetBool("dynamic_toolsets"), - ReadOnly: viper.GetBool("read-only"), + ReadOnly: viper.GetBool(readOnlyKey), ExportTranslations: viper.GetBool("export-translations"), EnableCommandLogging: viper.GetBool("enable-command-logging"), LogFilePath: viper.GetString("log-file"), @@ -70,7 +73,7 @@ func init() { // Add global flags that will be shared by all commands rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets") - rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations") + rootCmd.PersistentFlags().Bool(readOnlyKey, false, "Restrict the server to read-only operations") rootCmd.PersistentFlags().String("log-file", "", "Path to log file") rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file") rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") @@ -79,7 +82,7 @@ func init() { // Bind flag to viper _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) _ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets")) - _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) + _ = viper.BindPFlag(readOnlyKey, rootCmd.PersistentFlags().Lookup(readOnlyKey)) _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file")) _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 9d81e9010..ab6b1f4bb 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -2,12 +2,16 @@ package errors import ( "context" + "errors" "fmt" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" ) +// Error variables for context errors +var errContextMissingGitHubCtxErrors = errors.New("context does not contain GitHubCtxErrors") + type GitHubAPIError struct { Message string `json:"message"` Response *github.Response `json:"-"` @@ -71,7 +75,7 @@ func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) { if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok { return val.api, nil // return the slice of API errors from the context } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } // GetGitHubGraphQLErrors retrieves the slice of GitHubGraphQLErrors from the context. @@ -79,7 +83,7 @@ func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok { return val.graphQL, nil // return the slice of GraphQL errors from the context } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) { @@ -95,7 +99,7 @@ func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (conte val.api = append(val.api, err) // append the error to the existing slice in the context return ctx, nil } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) { @@ -103,7 +107,7 @@ func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError val.graphQL = append(val.graphQL, err) // append the error to the existing slice in the context return ctx, nil } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } // NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware