From 1dea4458ea19caacea767e2800d12dfa132b6442 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 9 Apr 2025 01:14:30 +0200 Subject: [PATCH] feat: partition tools by product/feature --- cmd/github-mcp-server/main.go | 57 ++++++-- go.mod | 2 +- go.sum | 4 +- pkg/github/context_tools.go | 49 +++++++ pkg/github/context_tools_test.go | 132 ++++++++++++++++++ pkg/github/dynamic_tools.go | 125 +++++++++++++++++ pkg/github/resources.go | 14 ++ pkg/github/server.go | 106 +------------- pkg/github/server_test.go | 123 ----------------- pkg/github/tools.go | 117 ++++++++++++++++ pkg/toolsets/toolsets.go | 154 +++++++++++++++++++++ pkg/toolsets/toolsets_test.go | 230 +++++++++++++++++++++++++++++++ pkg/translations/translations.go | 3 - third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 16 files changed, 877 insertions(+), 245 deletions(-) create mode 100644 pkg/github/context_tools.go create mode 100644 pkg/github/context_tools_test.go create mode 100644 pkg/github/dynamic_tools.go create mode 100644 pkg/github/resources.go create mode 100644 pkg/github/tools.go create mode 100644 pkg/toolsets/toolsets.go create mode 100644 pkg/toolsets/toolsets_test.go diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 354ec3a9..4d5368ec 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -44,12 +44,16 @@ var ( if err != nil { stdlog.Fatal("Failed to initialize logger:", err) } + + enabledToolsets := viper.GetStringSlice("toolsets") + logCommands := viper.GetBool("enable-command-logging") cfg := runConfig{ readOnly: readOnly, logger: logger, logCommands: logCommands, exportTranslations: exportTranslations, + enabledToolsets: enabledToolsets, } if err := runStdioServer(cfg); err != nil { stdlog.Fatal("failed to run stdio server:", err) @@ -62,6 +66,8 @@ func init() { cobra.OnInitialize(initConfig) // 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().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") @@ -69,11 +75,13 @@ func init() { rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") // 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("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")) - _ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host")) + _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host")) // Add subcommands rootCmd.AddCommand(stdioCmd) @@ -81,7 +89,7 @@ func init() { func initConfig() { // Initialize Viper configuration - viper.SetEnvPrefix("APP") + viper.SetEnvPrefix("github") viper.AutomaticEnv() } @@ -107,6 +115,7 @@ type runConfig struct { logger *log.Logger logCommands bool exportTranslations bool + enabledToolsets []string } func runStdioServer(cfg runConfig) error { @@ -115,18 +124,14 @@ func runStdioServer(cfg runConfig) error { defer stop() // Create GH client - token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN") + token := viper.GetString("personal_access_token") if token == "" { cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") } ghClient := gogithub.NewClient(nil).WithAuthToken(token) ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version) - // Check GH_HOST env var first, then fall back to viper config - host := os.Getenv("GH_HOST") - if host == "" { - host = viper.GetString("gh-host") - } + host := viper.GetString("host") if host != "" { var err error @@ -149,8 +154,40 @@ func runStdioServer(cfg runConfig) error { hooks := &server.Hooks{ OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, } - // Create - ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks)) + // Create server + ghServer := github.NewServer(version, server.WithHooks(hooks)) + + enabled := cfg.enabledToolsets + dynamic := viper.GetBool("dynamic_toolsets") + if dynamic { + // filter "all" from the enabled toolsets + enabled = make([]string, 0, len(cfg.enabledToolsets)) + for _, toolset := range cfg.enabledToolsets { + if toolset != "all" { + enabled = append(enabled, toolset) + } + } + } + + // Create default toolsets + toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t) + context := github.InitContextToolset(getClient, t) + + if err != nil { + stdlog.Fatal("Failed to initialize toolsets:", err) + } + + // Register resources with the server + github.RegisterResources(ghServer, getClient, t) + // Register the tools with the server + toolsets.RegisterTools(ghServer) + context.RegisterTools(ghServer) + + if dynamic { + dynamic := github.InitDynamicToolset(ghServer, toolsets, t) + dynamic.RegisterTools(ghServer) + } + stdioServer := server.NewStdioServer(ghServer) stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) diff --git a/go.mod b/go.mod index 858690cd..7c09fba9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/docker v28.0.4+incompatible github.com/google/go-cmp v0.7.0 github.com/google/go-github/v69 v69.2.0 - github.com/mark3labs/mcp-go v0.18.0 + github.com/mark3labs/mcp-go v0.20.1 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 19d368de..3378b4fd 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao= -github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw= +github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go new file mode 100644 index 00000000..1c91d703 --- /dev/null +++ b/pkg/github/context_tools.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// GetMe creates a tool to get details of the authenticated user. +func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_me", + mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), + mcp.WithString("reason", + mcp.Description("Optional: reason the session was created"), + ), + ), + func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + user, resp, err := client.Users.Get(ctx, "") + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil + } + + r, err := json.Marshal(user) + if err != nil { + return nil, fmt.Errorf("failed to marshal user: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go new file mode 100644 index 00000000..c9d220dd --- /dev/null +++ b/pkg/github/context_tools_test.go @@ -0,0 +1,132 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_GetMe(t *testing.T) { + // Verify tool definition + mockClient := github.NewClient(nil) + tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_me", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "reason") + assert.Empty(t, tool.InputSchema.Required) // No required parameters + + // Setup mock user response + mockUser := &github.User{ + Login: github.Ptr("testuser"), + Name: github.Ptr("Test User"), + Email: github.Ptr("test@example.com"), + Bio: github.Ptr("GitHub user for testing"), + Company: github.Ptr("Test Company"), + Location: github.Ptr("Test Location"), + HTMLURL: github.Ptr("https://github.com/testuser"), + CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, + Type: github.Ptr("User"), + Plan: &github.Plan{ + Name: github.Ptr("pro"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedUser *github.User + expectedErrMsg string + }{ + { + name: "successful get user", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + mockUser, + ), + ), + requestArgs: map[string]interface{}{}, + expectError: false, + expectedUser: mockUser, + }, + { + name: "successful get user with reason", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + mockUser, + ), + ), + requestArgs: map[string]interface{}{ + "reason": "Testing API", + }, + expectError: false, + expectedUser: mockUser, + }, + { + name: "get user fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUser, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message": "Unauthorized"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{}, + expectError: true, + expectedErrMsg: "failed to get user", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse result and get text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedUser github.User + err = json.Unmarshal([]byte(textContent.Text), &returnedUser) + require.NoError(t, err) + + // Verify user details + assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login) + assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name) + assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email) + assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio) + assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL) + assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type) + }) + } +} diff --git a/pkg/github/dynamic_tools.go b/pkg/github/dynamic_tools.go new file mode 100644 index 00000000..d4d5f27a --- /dev/null +++ b/pkg/github/dynamic_tools.go @@ -0,0 +1,125 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/github/github-mcp-server/pkg/toolsets" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func ToolsetEnum(toolsetGroup *toolsets.ToolsetGroup) mcp.PropertyOption { + toolsetNames := make([]string, 0, len(toolsetGroup.Toolsets)) + for name := range toolsetGroup.Toolsets { + toolsetNames = append(toolsetNames, name) + } + return mcp.Enum(toolsetNames...) +} + +func EnableToolset(s *server.MCPServer, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("enable_toolset", + mcp.WithDescription(t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable")), + mcp.WithString("toolset", + mcp.Required(), + mcp.Description("The name of the toolset to enable"), + ToolsetEnum(toolsetGroup), + ), + ), + func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsets back to a map for JSON serialization + toolsetName, err := requiredParam[string](request, "toolset") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + toolset := toolsetGroup.Toolsets[toolsetName] + if toolset == nil { + return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil + } + if toolset.Enabled { + return mcp.NewToolResultText(fmt.Sprintf("Toolset %s is already enabled", toolsetName)), nil + } + + toolset.Enabled = true + + // caution: this currently affects the global tools and notifies all clients: + // + // Send notification to all initialized sessions + // s.sendNotificationToAllClients("notifications/tools/list_changed", nil) + s.AddTools(toolset.GetActiveTools()...) + + return mcp.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil + } +} + +func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_available_toolsets", + mcp.WithDescription(t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call")), + ), + func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsetGroup back to a map for JSON serialization + + payload := []map[string]string{} + + for name, ts := range toolsetGroup.Toolsets { + { + t := map[string]string{ + "name": name, + "description": ts.Description, + "can_enable": "true", + "currently_enabled": fmt.Sprintf("%t", ts.Enabled), + } + payload = append(payload, t) + } + } + + r, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal features: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_toolset_tools", + mcp.WithDescription(t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task")), + mcp.WithString("toolset", + mcp.Required(), + mcp.Description("The name of the toolset you want to get the tools for"), + ToolsetEnum(toolsetGroup), + ), + ), + func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsetGroup back to a map for JSON serialization + toolsetName, err := requiredParam[string](request, "toolset") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + toolset := toolsetGroup.Toolsets[toolsetName] + if toolset == nil { + return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil + } + payload := []map[string]string{} + + for _, st := range toolset.GetAvailableTools() { + tool := map[string]string{ + "name": st.Tool.Name, + "description": st.Tool.Description, + "can_enable": "true", + "toolset": toolsetName, + } + payload = append(payload, tool) + } + + r, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal features: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/resources.go b/pkg/github/resources.go new file mode 100644 index 00000000..774261e9 --- /dev/null +++ b/pkg/github/resources.go @@ -0,0 +1,14 @@ +package github + +import ( + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/server" +) + +func RegisterResources(s *server.MCPServer, getClient GetClientFn, t translations.TranslationHelperFunc) { + s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) +} diff --git a/pkg/github/server.go b/pkg/github/server.go index da916b98..e4c24171 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -1,25 +1,20 @@ package github import ( - "context" - "encoding/json" "errors" "fmt" - "io" - "net/http" - "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -type GetClientFn func(context.Context) (*github.Client, error) - // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc, opts ...server.ServerOption) *server.MCPServer { + +func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { // Add default options defaultOpts := []server.ServerOption{ + server.WithToolCapabilities(true), server.WithResourceCapabilities(true, true), server.WithLogging(), } @@ -31,104 +26,9 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati version, opts..., ) - - // Add GitHub Resources - s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) - - // Add GitHub tools - Issues - s.AddTool(GetIssue(getClient, t)) - s.AddTool(SearchIssues(getClient, t)) - s.AddTool(ListIssues(getClient, t)) - s.AddTool(GetIssueComments(getClient, t)) - if !readOnly { - s.AddTool(CreateIssue(getClient, t)) - s.AddTool(AddIssueComment(getClient, t)) - s.AddTool(UpdateIssue(getClient, t)) - } - - // Add GitHub tools - Pull Requests - s.AddTool(GetPullRequest(getClient, t)) - s.AddTool(ListPullRequests(getClient, t)) - s.AddTool(GetPullRequestFiles(getClient, t)) - s.AddTool(GetPullRequestStatus(getClient, t)) - s.AddTool(GetPullRequestComments(getClient, t)) - s.AddTool(GetPullRequestReviews(getClient, t)) - if !readOnly { - s.AddTool(MergePullRequest(getClient, t)) - s.AddTool(UpdatePullRequestBranch(getClient, t)) - s.AddTool(CreatePullRequestReview(getClient, t)) - s.AddTool(CreatePullRequest(getClient, t)) - s.AddTool(UpdatePullRequest(getClient, t)) - s.AddTool(AddPullRequestReviewComment(getClient, t)) - } - - // Add GitHub tools - Repositories - s.AddTool(SearchRepositories(getClient, t)) - s.AddTool(GetFileContents(getClient, t)) - s.AddTool(GetCommit(getClient, t)) - s.AddTool(ListCommits(getClient, t)) - s.AddTool(ListBranches(getClient, t)) - if !readOnly { - s.AddTool(CreateOrUpdateFile(getClient, t)) - s.AddTool(CreateRepository(getClient, t)) - s.AddTool(ForkRepository(getClient, t)) - s.AddTool(CreateBranch(getClient, t)) - s.AddTool(PushFiles(getClient, t)) - } - - // Add GitHub tools - Search - s.AddTool(SearchCode(getClient, t)) - s.AddTool(SearchUsers(getClient, t)) - - // Add GitHub tools - Users - s.AddTool(GetMe(getClient, t)) - - // Add GitHub tools - Code Scanning - s.AddTool(GetCodeScanningAlert(getClient, t)) - s.AddTool(ListCodeScanningAlerts(getClient, t)) return s } -// GetMe creates a tool to get details of the authenticated user. -func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_me", - mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), - mcp.WithString("reason", - mcp.Description("Optional: reason the session was created"), - ), - ), - func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - user, resp, err := client.Users.Get(ctx, "") - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil - } - - r, err := json.Marshal(user) - if err != nil { - return nil, fmt.Errorf("failed to marshal user: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - // 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) { diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 3ee9851a..58bcb9db 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -2,17 +2,11 @@ package github import ( "context" - "encoding/json" "fmt" - "net/http" "testing" - "time" - "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" - "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func stubGetClientFn(client *github.Client) GetClientFn { @@ -21,123 +15,6 @@ func stubGetClientFn(client *github.Client) GetClientFn { } } -func Test_GetMe(t *testing.T) { - // Verify tool definition - mockClient := github.NewClient(nil) - tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) - - assert.Equal(t, "get_me", tool.Name) - assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "reason") - assert.Empty(t, tool.InputSchema.Required) // No required parameters - - // Setup mock user response - mockUser := &github.User{ - Login: github.Ptr("testuser"), - Name: github.Ptr("Test User"), - Email: github.Ptr("test@example.com"), - Bio: github.Ptr("GitHub user for testing"), - Company: github.Ptr("Test Company"), - Location: github.Ptr("Test Location"), - HTMLURL: github.Ptr("https://github.com/testuser"), - CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, - Type: github.Ptr("User"), - Plan: &github.Plan{ - Name: github.Ptr("pro"), - }, - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedUser *github.User - expectedErrMsg string - }{ - { - name: "successful get user", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetUser, - mockUser, - ), - ), - requestArgs: map[string]interface{}{}, - expectError: false, - expectedUser: mockUser, - }, - { - name: "successful get user with reason", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetUser, - mockUser, - ), - ), - requestArgs: map[string]interface{}{ - "reason": "Testing API", - }, - expectError: false, - expectedUser: mockUser, - }, - { - name: "get user fails", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatchHandler( - mock.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"message": "Unauthorized"}`)) - }), - ), - ), - requestArgs: map[string]interface{}{}, - expectError: true, - expectedErrMsg: "failed to get user", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(context.Background(), request) - - // Verify results - if tc.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrMsg) - return - } - - require.NoError(t, err) - - // Parse result and get text content if no error - textContent := getTextResult(t, result) - - // Unmarshal and verify the result - var returnedUser github.User - err = json.Unmarshal([]byte(textContent.Text), &returnedUser) - require.NoError(t, err) - - // Verify user details - assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login) - assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name) - assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email) - assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio) - assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL) - assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type) - }) - } -} - func Test_IsAcceptedError(t *testing.T) { tests := []struct { name string diff --git a/pkg/github/tools.go b/pkg/github/tools.go new file mode 100644 index 00000000..ce10c4ad --- /dev/null +++ b/pkg/github/tools.go @@ -0,0 +1,117 @@ +package github + +import ( + "context" + + "github.com/github/github-mcp-server/pkg/toolsets" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/server" +) + +type GetClientFn func(context.Context) (*github.Client, error) + +var DefaultTools = []string{"all"} + +func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, t translations.TranslationHelperFunc) (*toolsets.ToolsetGroup, error) { + // Create a new toolset group + tsg := toolsets.NewToolsetGroup(readOnly) + + // Define all available features with their default state (disabled) + // Create toolsets + repos := toolsets.NewToolset("repos", "GitHub Repository related tools"). + AddReadTools( + toolsets.NewServerTool(SearchRepositories(getClient, t)), + toolsets.NewServerTool(GetFileContents(getClient, t)), + toolsets.NewServerTool(ListCommits(getClient, t)), + toolsets.NewServerTool(SearchCode(getClient, t)), + toolsets.NewServerTool(GetCommit(getClient, t)), + toolsets.NewServerTool(ListBranches(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), + toolsets.NewServerTool(CreateRepository(getClient, t)), + toolsets.NewServerTool(ForkRepository(getClient, t)), + toolsets.NewServerTool(CreateBranch(getClient, t)), + toolsets.NewServerTool(PushFiles(getClient, t)), + ) + issues := toolsets.NewToolset("issues", "GitHub Issues related tools"). + AddReadTools( + toolsets.NewServerTool(GetIssue(getClient, t)), + toolsets.NewServerTool(SearchIssues(getClient, t)), + toolsets.NewServerTool(ListIssues(getClient, t)), + toolsets.NewServerTool(GetIssueComments(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(CreateIssue(getClient, t)), + toolsets.NewServerTool(AddIssueComment(getClient, t)), + toolsets.NewServerTool(UpdateIssue(getClient, t)), + ) + users := toolsets.NewToolset("users", "GitHub User related tools"). + AddReadTools( + toolsets.NewServerTool(SearchUsers(getClient, t)), + ) + pullRequests := toolsets.NewToolset("pull_requests", "GitHub Pull Request related tools"). + AddReadTools( + toolsets.NewServerTool(GetPullRequest(getClient, t)), + toolsets.NewServerTool(ListPullRequests(getClient, t)), + toolsets.NewServerTool(GetPullRequestFiles(getClient, t)), + toolsets.NewServerTool(GetPullRequestStatus(getClient, t)), + toolsets.NewServerTool(GetPullRequestComments(getClient, t)), + toolsets.NewServerTool(GetPullRequestReviews(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(MergePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), + toolsets.NewServerTool(CreatePullRequestReview(getClient, t)), + toolsets.NewServerTool(CreatePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequest(getClient, t)), + toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)), + ) + codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning"). + AddReadTools( + toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), + toolsets.NewServerTool(ListCodeScanningAlerts(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") + + // Add toolsets to the group + tsg.AddToolset(repos) + tsg.AddToolset(issues) + tsg.AddToolset(users) + tsg.AddToolset(pullRequests) + tsg.AddToolset(codeSecurity) + tsg.AddToolset(experiments) + // Enable the requested features + + if err := tsg.EnableToolsets(passedToolsets); err != nil { + return nil, err + } + + return tsg, nil +} + +func InitContextToolset(getClient GetClientFn, t translations.TranslationHelperFunc) *toolsets.Toolset { + // Create a new context toolset + contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in"). + AddReadTools( + toolsets.NewServerTool(GetMe(getClient, t)), + ) + contextTools.Enabled = true + return contextTools +} + +// InitDynamicToolset creates a dynamic toolset that can be used to enable other toolsets, and so requires the server and toolset group as arguments +func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset { + // Create a new dynamic toolset + // Need to add the dynamic toolset last so it can be used to enable other toolsets + dynamicToolSelection := toolsets.NewToolset("dynamic", "Discover GitHub MCP tools that can help achieve tasks by enabling additional sets of tools, you can control the enablement of any toolset to access its tools when this toolset is enabled."). + AddReadTools( + toolsets.NewServerTool(ListAvailableToolsets(tsg, t)), + toolsets.NewServerTool(GetToolsetsTools(tsg, t)), + toolsets.NewServerTool(EnableToolset(s, tsg, t)), + ) + dynamicToolSelection.Enabled = true + return dynamicToolSelection +} diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go new file mode 100644 index 00000000..d4397fc9 --- /dev/null +++ b/pkg/toolsets/toolsets.go @@ -0,0 +1,154 @@ +package toolsets + +import ( + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerTool { + return server.ServerTool{Tool: tool, Handler: handler} +} + +type Toolset struct { + Name string + Description string + Enabled bool + readOnly bool + writeTools []server.ServerTool + readTools []server.ServerTool +} + +func (t *Toolset) GetActiveTools() []server.ServerTool { + if t.Enabled { + if t.readOnly { + return t.readTools + } + return append(t.readTools, t.writeTools...) + } + return nil +} + +func (t *Toolset) GetAvailableTools() []server.ServerTool { + if t.readOnly { + return t.readTools + } + return append(t.readTools, t.writeTools...) +} + +func (t *Toolset) RegisterTools(s *server.MCPServer) { + if !t.Enabled { + return + } + for _, tool := range t.readTools { + s.AddTool(tool.Tool, tool.Handler) + } + if !t.readOnly { + for _, tool := range t.writeTools { + s.AddTool(tool.Tool, tool.Handler) + } + } +} + +func (t *Toolset) SetReadOnly() { + // Set the toolset to read-only + t.readOnly = true +} + +func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset { + // Silently ignore if the toolset is read-only to avoid any breach of that contract + if !t.readOnly { + t.writeTools = append(t.writeTools, tools...) + } + return t +} + +func (t *Toolset) AddReadTools(tools ...server.ServerTool) *Toolset { + t.readTools = append(t.readTools, tools...) + return t +} + +type ToolsetGroup struct { + Toolsets map[string]*Toolset + everythingOn bool + readOnly bool +} + +func NewToolsetGroup(readOnly bool) *ToolsetGroup { + return &ToolsetGroup{ + Toolsets: make(map[string]*Toolset), + everythingOn: false, + readOnly: readOnly, + } +} + +func (tg *ToolsetGroup) AddToolset(ts *Toolset) { + if tg.readOnly { + ts.SetReadOnly() + } + tg.Toolsets[ts.Name] = ts +} + +func NewToolset(name string, description string) *Toolset { + return &Toolset{ + Name: name, + Description: description, + Enabled: false, + readOnly: false, + } +} + +func (tg *ToolsetGroup) IsEnabled(name string) bool { + // If everythingOn is true, all features are enabled + if tg.everythingOn { + return true + } + + feature, exists := tg.Toolsets[name] + if !exists { + return false + } + return feature.Enabled +} + +func (tg *ToolsetGroup) EnableToolsets(names []string) error { + // Special case for "all" + for _, name := range names { + if name == "all" { + tg.everythingOn = true + break + } + err := tg.EnableToolset(name) + if err != nil { + return err + } + } + // Do this after to ensure all toolsets are enabled if "all" is present anywhere in list + if tg.everythingOn { + for name := range tg.Toolsets { + err := tg.EnableToolset(name) + if err != nil { + return err + } + } + return nil + } + return nil +} + +func (tg *ToolsetGroup) EnableToolset(name string) error { + toolset, exists := tg.Toolsets[name] + if !exists { + return fmt.Errorf("toolset %s does not exist", name) + } + toolset.Enabled = true + tg.Toolsets[name] = toolset + return nil +} + +func (tg *ToolsetGroup) RegisterTools(s *server.MCPServer) { + for _, toolset := range tg.Toolsets { + toolset.RegisterTools(s) + } +} diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go new file mode 100644 index 00000000..7ece1df1 --- /dev/null +++ b/pkg/toolsets/toolsets_test.go @@ -0,0 +1,230 @@ +package toolsets + +import ( + "testing" +) + +func TestNewToolsetGroup(t *testing.T) { + tsg := NewToolsetGroup(false) + if tsg == nil { + t.Fatal("Expected NewToolsetGroup to return a non-nil pointer") + } + if tsg.Toolsets == nil { + t.Fatal("Expected Toolsets map to be initialized") + } + if len(tsg.Toolsets) != 0 { + t.Fatalf("Expected Toolsets map to be empty, got %d items", len(tsg.Toolsets)) + } + if tsg.everythingOn { + t.Fatal("Expected everythingOn to be initialized as false") + } +} + +func TestAddToolset(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test adding a toolset + toolset := NewToolset("test-toolset", "A test toolset") + toolset.Enabled = true + tsg.AddToolset(toolset) + + // Verify toolset was added correctly + if len(tsg.Toolsets) != 1 { + t.Errorf("Expected 1 toolset, got %d", len(tsg.Toolsets)) + } + + toolset, exists := tsg.Toolsets["test-toolset"] + if !exists { + t.Fatal("Feature was not added to the map") + } + + if toolset.Name != "test-toolset" { + t.Errorf("Expected toolset name to be 'test-toolset', got '%s'", toolset.Name) + } + + if toolset.Description != "A test toolset" { + t.Errorf("Expected toolset description to be 'A test toolset', got '%s'", toolset.Description) + } + + if !toolset.Enabled { + t.Error("Expected toolset to be enabled") + } + + // Test adding another toolset + anotherToolset := NewToolset("another-toolset", "Another test toolset") + tsg.AddToolset(anotherToolset) + + if len(tsg.Toolsets) != 2 { + t.Errorf("Expected 2 toolsets, got %d", len(tsg.Toolsets)) + } + + // Test overriding existing toolset + updatedToolset := NewToolset("test-toolset", "Updated description") + tsg.AddToolset(updatedToolset) + + toolset = tsg.Toolsets["test-toolset"] + if toolset.Description != "Updated description" { + t.Errorf("Expected toolset description to be updated to 'Updated description', got '%s'", toolset.Description) + } + + if toolset.Enabled { + t.Error("Expected toolset to be disabled after update") + } +} + +func TestIsEnabled(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test with non-existent toolset + if tsg.IsEnabled("non-existent") { + t.Error("Expected IsEnabled to return false for non-existent toolset") + } + + // Test with disabled toolset + disabledToolset := NewToolset("disabled-toolset", "A disabled toolset") + tsg.AddToolset(disabledToolset) + if tsg.IsEnabled("disabled-toolset") { + t.Error("Expected IsEnabled to return false for disabled toolset") + } + + // Test with enabled toolset + enabledToolset := NewToolset("enabled-toolset", "An enabled toolset") + enabledToolset.Enabled = true + tsg.AddToolset(enabledToolset) + if !tsg.IsEnabled("enabled-toolset") { + t.Error("Expected IsEnabled to return true for enabled toolset") + } +} + +func TestEnableFeature(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test enabling non-existent toolset + err := tsg.EnableToolset("non-existent") + if err == nil { + t.Error("Expected error when enabling non-existent toolset") + } + + // Test enabling toolset + testToolset := NewToolset("test-toolset", "A test toolset") + tsg.AddToolset(testToolset) + + if tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be disabled initially") + } + + err = tsg.EnableToolset("test-toolset") + if err != nil { + t.Errorf("Expected no error when enabling toolset, got: %v", err) + } + + if !tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be enabled after EnableFeature call") + } + + // Test enabling already enabled toolset + err = tsg.EnableToolset("test-toolset") + if err != nil { + t.Errorf("Expected no error when enabling already enabled toolset, got: %v", err) + } +} + +func TestEnableToolsets(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Prepare toolsets + toolset1 := NewToolset("toolset1", "Feature 1") + toolset2 := NewToolset("toolset2", "Feature 2") + tsg.AddToolset(toolset1) + tsg.AddToolset(toolset2) + + // Test enabling multiple toolsets + err := tsg.EnableToolsets([]string{"toolset1", "toolset2"}) + if err != nil { + t.Errorf("Expected no error when enabling toolsets, got: %v", err) + } + + if !tsg.IsEnabled("toolset1") { + t.Error("Expected toolset1 to be enabled") + } + + if !tsg.IsEnabled("toolset2") { + t.Error("Expected toolset2 to be enabled") + } + + // Test with non-existent toolset in the list + err = tsg.EnableToolsets([]string{"toolset1", "non-existent"}) + if err == nil { + t.Error("Expected error when enabling list with non-existent toolset") + } + + // Test with empty list + err = tsg.EnableToolsets([]string{}) + if err != nil { + t.Errorf("Expected no error with empty toolset list, got: %v", err) + } + + // Test enabling everything through EnableToolsets + tsg = NewToolsetGroup(false) + err = tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'all', got: %v", err) + } + + if !tsg.everythingOn { + t.Error("Expected everythingOn to be true after enabling 'all' via EnableToolsets") + } +} + +func TestEnableEverything(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Add a disabled toolset + testToolset := NewToolset("test-toolset", "A test toolset") + tsg.AddToolset(testToolset) + + // Verify it's disabled + if tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be disabled initially") + } + + // Enable "all" + err := tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'eall', got: %v", err) + } + + // Verify everythingOn was set + if !tsg.everythingOn { + t.Error("Expected everythingOn to be true after enabling 'eall'") + } + + // Verify the previously disabled toolset is now enabled + if !tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be enabled when everythingOn is true") + } + + // Verify a non-existent toolset is also enabled + if !tsg.IsEnabled("non-existent") { + t.Error("Expected non-existent toolset to be enabled when everythingOn is true") + } +} + +func TestIsEnabledWithEverythingOn(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Enable "everything" + err := tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'all', got: %v", err) + } + + // Test that any toolset name returns true with IsEnabled + if !tsg.IsEnabled("some-toolset") { + t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") + } + + if !tsg.IsEnabled("another-toolset") { + t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") + } +} diff --git a/pkg/translations/translations.go b/pkg/translations/translations.go index 6d910525..741ee2b5 100644 --- a/pkg/translations/translations.go +++ b/pkg/translations/translations.go @@ -20,9 +20,6 @@ func TranslationHelper() (TranslationHelperFunc, func()) { var translationKeyMap = map[string]string{} v := viper.New() - v.SetEnvPrefix("GITHUB_MCP_") - v.AutomaticEnv() - // Load from JSON file v.SetConfigName("github-mcp-server-config") v.SetConfigType("json") diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 80c6d1c4..389bb966 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 80c6d1c4..389bb966 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 5fc973d7..96d037cc 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -14,7 +14,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))