From bc897a1518962d0637435b4e6823362fbdb18046 Mon Sep 17 00:00:00 2001 From: Avinash Sridhar Date: Fri, 18 Apr 2025 17:16:56 -0400 Subject: [PATCH] Fix merge conflicts --- pkg/github/server.go | 257 +++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 14 +++ 2 files changed, 271 insertions(+) diff --git a/pkg/github/server.go b/pkg/github/server.go index e69de29b..c51e4732 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -0,0 +1,257 @@ +package github + +import ( + "errors" + "fmt" + + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// NewServer creates a new GitHub MCP server with the specified GH client and logger. + +func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { + // Add default options + defaultOpts := []server.ServerOption{ + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true, true), + server.WithLogging(), + } + opts = append(defaultOpts, opts...) + + // Create a new MCP server + s := server.NewMCPServer( + "github-mcp-server", + version, + opts..., + ) + return s +} + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := r.Params.Arguments[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// requiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func requiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := r.Params.Arguments[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + if _, ok := r.Params.Arguments[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if r.Params.Arguments[p].(T) == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + + } + + return r.Params.Arguments[p].(T), nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(r mcp.CallToolRequest, p string) (int, error) { + v, err := requiredParam[float64](r, p) + if err != nil { + return 0, err + } + return int(v), nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := r.Params.Arguments[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := r.Params.Arguments[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, r.Params.Arguments[p]) + } + + return r.Params.Arguments[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) { + v, err := OptionalParam[float64](r, p) + if err != nil { + return 0, err + } + return int(v), nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to optionalIntParam, but it also takes a default value. +func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) { + v, err := OptionalIntParam(r, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to optionalParam, but it also takes a default value. +func OptionalBoolParamWithDefault(r mcp.CallToolRequest, p string, d bool) (bool, error) { + v, err := OptionalParam[bool](r, p) + if err != nil { + return false, err + } + if !v { + return d, nil + } + return v, nil +} + +// OptionalStringParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalStringParam(r mcp.CallToolRequest, p string) (string, error) { + v, err := OptionalParam[string](r, p) + if err != nil { + return "", err + } + if v == "" { + return "", nil + } + return v, nil +} + +// OptionalStringParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to optionalParam, but it also takes a default value. +func OptionalStringParamWithDefault(r mcp.CallToolRequest, p string, d string) (string, error) { + v, err := OptionalParam[string](r, p) + if err != nil { + return "", err + } + if v == "" { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := r.Params.Arguments[p]; !ok { + return []string{}, nil + } + + switch v := r.Params.Arguments[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, r.Params.Arguments[p]) + } +} + +// WithPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool. +// The "page" parameter is optional, min 1. The "perPage" parameter is optional, min 1, max 100. +func WithPagination() mcp.ToolOption { + return func(tool *mcp.Tool) { + mcp.WithNumber("page", + mcp.Description("Page number for pagination (min 1)"), + mcp.Min(1), + )(tool) + + mcp.WithNumber("perPage", + mcp.Description("Results per page for pagination (min 1, max 100)"), + mcp.Min(1), + mcp.Max(100), + )(tool) + } +} + +type PaginationParams struct { + page int + perPage int +} + +// OptionalPaginationParams returns the "page" and "perPage" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(r, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(r, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + page: page, + perPage: perPage, + }, nil +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 35dabaef..fd0f231b 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -78,6 +78,19 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), ) + + notifications := toolsets.NewToolset("notifications", "GitHub Notifications related tools"). + AddReadTools( + + toolsets.NewServerTool(MarkNotificationRead(getClient, t)), + toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)), + toolsets.NewServerTool(MarkNotificationDone(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(GetNotifications(getClient, t)), + toolsets.NewServerTool(GetNotificationThread(getClient, t)), + ) + // Keep experiments alive so the system doesn't error out when it's always enabled experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") @@ -88,6 +101,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, tsg.AddToolset(pullRequests) tsg.AddToolset(codeSecurity) tsg.AddToolset(secretProtection) + tsg.AddToolset(notifications) tsg.AddToolset(experiments) // Enable the requested features