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
119 changes: 75 additions & 44 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand Down
9 changes: 6 additions & 3 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"),
Expand All @@ -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")
Expand All @@ -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"))
Expand Down
12 changes: 8 additions & 4 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -71,15 +75,15 @@ 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.
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) {
Expand All @@ -95,15 +99,15 @@ 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) {
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
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
Expand Down